@ -0,0 +1,2 @@
|
||||
export 'bumper_noisy_behavior.dart';
|
||||
export 'scoring_behavior.dart';
|
@ -0,0 +1,14 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball/game/pinball_game.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
class BumperNoisyBehavior extends ContactBehavior with HasGameRef<PinballGame> {
|
||||
@override
|
||||
void beginContact(Object other, Contact contact) {
|
||||
super.beginContact(other, contact);
|
||||
gameRef.audio.bumper();
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
// ignore_for_file: avoid_renaming_method_parameters
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/effects.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// {@template scoring_behavior}
|
||||
/// Adds [_points] to the score and shows a text effect.
|
||||
///
|
||||
/// The behavior removes itself after the duration.
|
||||
/// {@endtemplate}
|
||||
class ScoringBehavior extends Component with HasGameRef<PinballGame> {
|
||||
/// {@macto scoring_behavior}
|
||||
ScoringBehavior({
|
||||
required Points points,
|
||||
required Vector2 position,
|
||||
double duration = 1,
|
||||
}) : _points = points,
|
||||
_position = position,
|
||||
_effectController = EffectController(
|
||||
duration: duration,
|
||||
);
|
||||
|
||||
final Points _points;
|
||||
final Vector2 _position;
|
||||
|
||||
final EffectController _effectController;
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
if (_effectController.completed) {
|
||||
removeFromParent();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
gameRef.read<GameBloc>().add(Scored(points: _points.value));
|
||||
await gameRef.firstChild<ZCanvasComponent>()!.add(
|
||||
ScoreComponent(
|
||||
points: _points,
|
||||
position: _position,
|
||||
effectController: _effectController,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// {@template scoring_contact_behavior}
|
||||
/// Adds points to the score when the [Ball] contacts the [parent].
|
||||
/// {@endtemplate}
|
||||
class ScoringContactBehavior extends ContactBehavior
|
||||
with HasGameRef<PinballGame> {
|
||||
/// {@macro scoring_contact_behavior}
|
||||
ScoringContactBehavior({
|
||||
required Points points,
|
||||
}) : _points = points;
|
||||
|
||||
final Points _points;
|
||||
|
||||
@override
|
||||
void beginContact(Object other, Contact contact) {
|
||||
super.beginContact(other, contact);
|
||||
if (other is! Ball) return;
|
||||
|
||||
parent.add(
|
||||
ScoringBehavior(
|
||||
points: _points,
|
||||
position: other.body.position,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:pinball/game/components/backbox/displays/displays.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// {@template backbox}
|
||||
/// The [Backbox] of the pinball machine.
|
||||
/// {@endtemplate}
|
||||
class Backbox extends PositionComponent with HasGameRef, ZIndex {
|
||||
/// {@macro backbox}
|
||||
Backbox()
|
||||
: super(
|
||||
position: Vector2(0, -87),
|
||||
anchor: Anchor.bottomCenter,
|
||||
children: [
|
||||
_BackboxSpriteComponent(),
|
||||
],
|
||||
) {
|
||||
zIndex = ZIndexes.backbox;
|
||||
}
|
||||
|
||||
/// Puts [InitialsInputDisplay] on the [Backbox].
|
||||
Future<void> initialsInput({
|
||||
required int score,
|
||||
required String characterIconPath,
|
||||
InitialsOnSubmit? onSubmit,
|
||||
}) async {
|
||||
removeAll(children.where((child) => child is! _BackboxSpriteComponent));
|
||||
await add(
|
||||
InitialsInputDisplay(
|
||||
score: score,
|
||||
characterIconPath: characterIconPath,
|
||||
onSubmit: onSubmit,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BackboxSpriteComponent extends SpriteComponent with HasGameRef {
|
||||
_BackboxSpriteComponent() : super(anchor: Anchor.bottomCenter);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
final sprite = Sprite(
|
||||
gameRef.images.fromCache(
|
||||
Assets.images.backbox.marquee.keyName,
|
||||
),
|
||||
);
|
||||
this.sprite = sprite;
|
||||
size = sprite.originalSize / 20;
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export 'initials_input_display.dart';
|
@ -0,0 +1,387 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
|
||||
/// Signature for the callback called when the used has
|
||||
/// submitted their initials on the [InitialsInputDisplay].
|
||||
typedef InitialsOnSubmit = void Function(String);
|
||||
|
||||
final _bodyTextPaint = TextPaint(
|
||||
style: const TextStyle(
|
||||
fontSize: 3,
|
||||
color: PinballColors.white,
|
||||
fontFamily: PinballFonts.pixeloidSans,
|
||||
),
|
||||
);
|
||||
|
||||
final _subtitleTextPaint = TextPaint(
|
||||
style: const TextStyle(
|
||||
fontSize: 1.8,
|
||||
color: PinballColors.white,
|
||||
fontFamily: PinballFonts.pixeloidSans,
|
||||
),
|
||||
);
|
||||
|
||||
/// {@template initials_input_display}
|
||||
/// Display that handles the user input on the game over view.
|
||||
/// {@endtemplate}
|
||||
// TODO(allisonryan0002): add mobile input buttons.
|
||||
class InitialsInputDisplay extends Component with HasGameRef {
|
||||
/// {@macro initials_input_display}
|
||||
InitialsInputDisplay({
|
||||
required int score,
|
||||
required String characterIconPath,
|
||||
InitialsOnSubmit? onSubmit,
|
||||
}) : _onSubmit = onSubmit,
|
||||
super(
|
||||
children: [
|
||||
_ScoreLabelTextComponent(),
|
||||
_ScoreTextComponent(score.formatScore()),
|
||||
_NameLabelTextComponent(),
|
||||
_CharacterIconSpriteComponent(characterIconPath),
|
||||
_DividerSpriteComponent(),
|
||||
_InstructionsComponent(),
|
||||
],
|
||||
);
|
||||
|
||||
final InitialsOnSubmit? _onSubmit;
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
for (var i = 0; i < 3; i++) {
|
||||
await add(
|
||||
InitialsLetterPrompt(
|
||||
position: Vector2(
|
||||
11.4 + (2.3 * i),
|
||||
-20,
|
||||
),
|
||||
hasFocus: i == 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await add(
|
||||
KeyboardInputController(
|
||||
keyUp: {
|
||||
LogicalKeyboardKey.arrowLeft: () => _movePrompt(true),
|
||||
LogicalKeyboardKey.arrowRight: () => _movePrompt(false),
|
||||
LogicalKeyboardKey.enter: _submit,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the current inputed initials
|
||||
String get initials => children
|
||||
.whereType<InitialsLetterPrompt>()
|
||||
.map((prompt) => prompt.char)
|
||||
.join();
|
||||
|
||||
bool _submit() {
|
||||
_onSubmit?.call(initials);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _movePrompt(bool left) {
|
||||
final prompts = children.whereType<InitialsLetterPrompt>().toList();
|
||||
|
||||
final current = prompts.firstWhere((prompt) => prompt.hasFocus)
|
||||
..hasFocus = false;
|
||||
var index = prompts.indexOf(current) + (left ? -1 : 1);
|
||||
index = min(max(0, index), prompts.length - 1);
|
||||
|
||||
prompts[index].hasFocus = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class _ScoreLabelTextComponent extends TextComponent
|
||||
with HasGameRef<PinballGame> {
|
||||
_ScoreLabelTextComponent()
|
||||
: super(
|
||||
anchor: Anchor.centerLeft,
|
||||
position: Vector2(-16.9, -24),
|
||||
textRenderer: _bodyTextPaint.copyWith(
|
||||
(style) => style.copyWith(
|
||||
color: PinballColors.red,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
text = gameRef.l10n.score;
|
||||
}
|
||||
}
|
||||
|
||||
class _ScoreTextComponent extends TextComponent {
|
||||
_ScoreTextComponent(String score)
|
||||
: super(
|
||||
text: score,
|
||||
anchor: Anchor.centerLeft,
|
||||
position: Vector2(-16.9, -20),
|
||||
textRenderer: _bodyTextPaint,
|
||||
);
|
||||
}
|
||||
|
||||
class _NameLabelTextComponent extends TextComponent
|
||||
with HasGameRef<PinballGame> {
|
||||
_NameLabelTextComponent()
|
||||
: super(
|
||||
anchor: Anchor.center,
|
||||
position: Vector2(11.4, -24),
|
||||
textRenderer: _bodyTextPaint.copyWith(
|
||||
(style) => style.copyWith(
|
||||
color: PinballColors.red,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
text = gameRef.l10n.name;
|
||||
}
|
||||
}
|
||||
|
||||
class _CharacterIconSpriteComponent extends SpriteComponent with HasGameRef {
|
||||
_CharacterIconSpriteComponent(String characterIconPath)
|
||||
: _characterIconPath = characterIconPath,
|
||||
super(
|
||||
anchor: Anchor.center,
|
||||
position: Vector2(8.4, -20),
|
||||
);
|
||||
|
||||
final String _characterIconPath;
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
final sprite = Sprite(gameRef.images.fromCache(_characterIconPath));
|
||||
this.sprite = sprite;
|
||||
size = sprite.originalSize / 20;
|
||||
}
|
||||
}
|
||||
|
||||
/// {@template initials_input_display}
|
||||
/// Display that handles the user input on the game over view.
|
||||
/// {@endtemplate}
|
||||
@visibleForTesting
|
||||
class InitialsLetterPrompt extends PositionComponent {
|
||||
/// {@macro initials_input_display}
|
||||
InitialsLetterPrompt({
|
||||
required Vector2 position,
|
||||
bool hasFocus = false,
|
||||
}) : _hasFocus = hasFocus,
|
||||
super(
|
||||
position: position,
|
||||
);
|
||||
|
||||
static const _alphabetCode = 65;
|
||||
static const _alphabetLength = 25;
|
||||
var _charIndex = 0;
|
||||
|
||||
bool _hasFocus;
|
||||
|
||||
late RectangleComponent _underscore;
|
||||
late TextComponent _input;
|
||||
late TimerComponent _underscoreBlinker;
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
_underscore = RectangleComponent(
|
||||
size: Vector2(1.9, 0.4),
|
||||
anchor: Anchor.center,
|
||||
position: Vector2(-0.1, 1.8),
|
||||
);
|
||||
|
||||
await add(_underscore);
|
||||
|
||||
_input = TextComponent(
|
||||
text: 'A',
|
||||
textRenderer: _bodyTextPaint,
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
await add(_input);
|
||||
|
||||
_underscoreBlinker = TimerComponent(
|
||||
period: 0.6,
|
||||
repeat: true,
|
||||
autoStart: _hasFocus,
|
||||
onTick: () {
|
||||
_underscore.paint.color = (_underscore.paint.color == Colors.white)
|
||||
? Colors.transparent
|
||||
: Colors.white;
|
||||
},
|
||||
);
|
||||
|
||||
await add(_underscoreBlinker);
|
||||
|
||||
await add(
|
||||
KeyboardInputController(
|
||||
keyUp: {
|
||||
LogicalKeyboardKey.arrowUp: () => _cycle(true),
|
||||
LogicalKeyboardKey.arrowDown: () => _cycle(false),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the current selected character
|
||||
String get char => String.fromCharCode(_alphabetCode + _charIndex);
|
||||
|
||||
bool _cycle(bool up) {
|
||||
if (_hasFocus) {
|
||||
final newCharCode =
|
||||
min(max(_charIndex + (up ? 1 : -1), 0), _alphabetLength);
|
||||
_input.text = String.fromCharCode(_alphabetCode + newCharCode);
|
||||
_charIndex = newCharCode;
|
||||
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Returns if this prompt has focus on it
|
||||
bool get hasFocus => _hasFocus;
|
||||
|
||||
/// Updates this prompt focus
|
||||
set hasFocus(bool hasFocus) {
|
||||
if (hasFocus) {
|
||||
_underscoreBlinker.timer.resume();
|
||||
} else {
|
||||
_underscoreBlinker.timer.pause();
|
||||
}
|
||||
_underscore.paint.color = Colors.white;
|
||||
_hasFocus = hasFocus;
|
||||
}
|
||||
}
|
||||
|
||||
class _DividerSpriteComponent extends SpriteComponent with HasGameRef {
|
||||
_DividerSpriteComponent()
|
||||
: super(
|
||||
anchor: Anchor.center,
|
||||
position: Vector2(0, -17),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
final sprite = Sprite(
|
||||
gameRef.images.fromCache(Assets.images.backbox.displayDivider.keyName),
|
||||
);
|
||||
this.sprite = sprite;
|
||||
size = sprite.originalSize / 20;
|
||||
}
|
||||
}
|
||||
|
||||
class _InstructionsComponent extends PositionComponent with HasGameRef {
|
||||
_InstructionsComponent()
|
||||
: super(
|
||||
anchor: Anchor.center,
|
||||
position: Vector2(0, -12.3),
|
||||
children: [
|
||||
_EnterInitialsTextComponent(),
|
||||
_ArrowsTextComponent(),
|
||||
_AndPressTextComponent(),
|
||||
_EnterReturnTextComponent(),
|
||||
_ToSubmitTextComponent(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _EnterInitialsTextComponent extends TextComponent
|
||||
with HasGameRef<PinballGame> {
|
||||
_EnterInitialsTextComponent()
|
||||
: super(
|
||||
anchor: Anchor.center,
|
||||
position: Vector2(0, -2.4),
|
||||
textRenderer: _subtitleTextPaint,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
text = gameRef.l10n.enterInitials;
|
||||
}
|
||||
}
|
||||
|
||||
class _ArrowsTextComponent extends TextComponent with HasGameRef<PinballGame> {
|
||||
_ArrowsTextComponent()
|
||||
: super(
|
||||
anchor: Anchor.center,
|
||||
position: Vector2(-13.2, 0),
|
||||
textRenderer: _subtitleTextPaint.copyWith(
|
||||
(style) => style.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
text = gameRef.l10n.arrows;
|
||||
}
|
||||
}
|
||||
|
||||
class _AndPressTextComponent extends TextComponent
|
||||
with HasGameRef<PinballGame> {
|
||||
_AndPressTextComponent()
|
||||
: super(
|
||||
anchor: Anchor.center,
|
||||
position: Vector2(-3.7, 0),
|
||||
textRenderer: _subtitleTextPaint,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
text = gameRef.l10n.andPress;
|
||||
}
|
||||
}
|
||||
|
||||
class _EnterReturnTextComponent extends TextComponent
|
||||
with HasGameRef<PinballGame> {
|
||||
_EnterReturnTextComponent()
|
||||
: super(
|
||||
anchor: Anchor.center,
|
||||
position: Vector2(10, 0),
|
||||
textRenderer: _subtitleTextPaint.copyWith(
|
||||
(style) => style.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
text = gameRef.l10n.enterReturn;
|
||||
}
|
||||
}
|
||||
|
||||
class _ToSubmitTextComponent extends TextComponent
|
||||
with HasGameRef<PinballGame> {
|
||||
_ToSubmitTextComponent()
|
||||
: super(
|
||||
anchor: Anchor.center,
|
||||
position: Vector2(0, 2.4),
|
||||
textRenderer: _subtitleTextPaint,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
text = gameRef.l10n.toSubmit;
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
// ignore_for_file: avoid_renaming_method_parameters
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// {@template scoring_behavior}
|
||||
/// Adds points to the score when the ball contacts the [parent].
|
||||
/// {@endtemplate}
|
||||
class ScoringBehavior extends ContactBehavior with HasGameRef<PinballGame> {
|
||||
/// {@macro scoring_behavior}
|
||||
ScoringBehavior({
|
||||
required Points points,
|
||||
}) : _points = points;
|
||||
|
||||
final Points _points;
|
||||
|
||||
@override
|
||||
void beginContact(Object other, Contact contact) {
|
||||
super.beginContact(other, contact);
|
||||
if (other is! Ball) return;
|
||||
|
||||
gameRef.read<GameBloc>().add(Scored(points: _points.value));
|
||||
gameRef.audio.score();
|
||||
gameRef.firstChild<ZCanvasComponent>()!.add(
|
||||
ScoreComponent(
|
||||
points: _points,
|
||||
position: other.body.position,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1 +1,2 @@
|
||||
export 'bloc/start_game_bloc.dart';
|
||||
export 'widgets/start_game_listener.dart';
|
||||
|
@ -0,0 +1,95 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball/how_to_play/how_to_play.dart';
|
||||
import 'package:pinball/select_character/select_character.dart';
|
||||
import 'package:pinball/start_game/start_game.dart';
|
||||
import 'package:pinball_audio/pinball_audio.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
|
||||
/// {@template start_game_listener}
|
||||
/// Listener that manages the display of dialogs for [StartGameStatus].
|
||||
///
|
||||
/// It's responsible for starting the game after pressing play button
|
||||
/// and playing a sound after the 'how to play' dialog.
|
||||
/// {@endtemplate}
|
||||
class StartGameListener extends StatelessWidget {
|
||||
/// {@macro start_game_listener}
|
||||
const StartGameListener({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
required PinballGame game,
|
||||
}) : _child = child,
|
||||
_game = game,
|
||||
super(key: key);
|
||||
|
||||
final Widget _child;
|
||||
final PinballGame _game;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<StartGameBloc, StartGameState>(
|
||||
listener: (context, state) {
|
||||
switch (state.status) {
|
||||
case StartGameStatus.initial:
|
||||
break;
|
||||
case StartGameStatus.selectCharacter:
|
||||
_onSelectCharacter(context);
|
||||
_game.gameFlowController.start();
|
||||
break;
|
||||
case StartGameStatus.howToPlay:
|
||||
_onHowToPlay(context);
|
||||
break;
|
||||
case StartGameStatus.play:
|
||||
break;
|
||||
}
|
||||
},
|
||||
child: _child,
|
||||
);
|
||||
}
|
||||
|
||||
void _onSelectCharacter(BuildContext context) {
|
||||
_showPinballDialog(
|
||||
context: context,
|
||||
child: const CharacterSelectionDialog(),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
}
|
||||
|
||||
void _onHowToPlay(BuildContext context) {
|
||||
final audio = context.read<PinballAudio>();
|
||||
|
||||
_showPinballDialog(
|
||||
context: context,
|
||||
child: HowToPlayDialog(
|
||||
onDismissCallback: () {
|
||||
context.read<StartGameBloc>().add(const HowToPlayFinished());
|
||||
audio.ioPinballVoiceOver();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPinballDialog({
|
||||
required BuildContext context,
|
||||
required Widget child,
|
||||
bool barrierDismissible = true,
|
||||
}) {
|
||||
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
|
||||
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierColor: PinballColors.transparent,
|
||||
barrierDismissible: barrierDismissible,
|
||||
builder: (_) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
height: gameWidgetWidth * 0.87,
|
||||
width: gameWidgetWidth,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
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: 955 KiB |
Before Width: | Height: | Size: 1.9 MiB |
Before Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 637 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 38 KiB |
@ -1,79 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
export 'backboard_game_over.dart';
|
||||
export 'backboard_letter_prompt.dart';
|
||||
export 'backboard_waiting.dart';
|
||||
|
||||
/// {@template backboard}
|
||||
/// The [Backboard] of the pinball machine.
|
||||
/// {@endtemplate}
|
||||
class Backboard extends PositionComponent with HasGameRef {
|
||||
/// {@macro backboard}
|
||||
Backboard({
|
||||
required Vector2 position,
|
||||
}) : super(
|
||||
position: position,
|
||||
anchor: Anchor.bottomCenter,
|
||||
);
|
||||
|
||||
/// {@macro backboard}
|
||||
///
|
||||
/// Returns a [Backboard] initialized in the waiting mode
|
||||
factory Backboard.waiting({
|
||||
required Vector2 position,
|
||||
}) {
|
||||
return Backboard(position: position)..waitingMode();
|
||||
}
|
||||
|
||||
/// {@macro backboard}
|
||||
///
|
||||
/// Returns a [Backboard] initialized in the game over mode
|
||||
factory Backboard.gameOver({
|
||||
required Vector2 position,
|
||||
required String characterIconPath,
|
||||
required int score,
|
||||
required BackboardOnSubmit onSubmit,
|
||||
}) {
|
||||
return Backboard(position: position)
|
||||
..gameOverMode(
|
||||
score: score,
|
||||
characterIconPath: characterIconPath,
|
||||
onSubmit: onSubmit,
|
||||
);
|
||||
}
|
||||
|
||||
/// [TextPaint] used on the [Backboard]
|
||||
static final textPaint = TextPaint(
|
||||
style: const TextStyle(
|
||||
fontSize: 6,
|
||||
color: Colors.white,
|
||||
fontFamily: PinballFonts.pixeloidSans,
|
||||
),
|
||||
);
|
||||
|
||||
/// Puts the Backboard in waiting mode, where the scoreboard is shown.
|
||||
Future<void> waitingMode() async {
|
||||
children.removeWhere((_) => true);
|
||||
await add(BackboardWaiting());
|
||||
}
|
||||
|
||||
/// Puts the Backboard in game over mode, where the score input is shown.
|
||||
Future<void> gameOverMode({
|
||||
required int score,
|
||||
required String characterIconPath,
|
||||
BackboardOnSubmit? onSubmit,
|
||||
}) async {
|
||||
children.removeWhere((_) => true);
|
||||
await add(
|
||||
BackboardGameOver(
|
||||
score: score,
|
||||
characterIconPath: characterIconPath,
|
||||
onSubmit: onSubmit,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,144 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// Signature for the callback called when the used has
|
||||
/// submettied their initials on the [BackboardGameOver]
|
||||
typedef BackboardOnSubmit = void Function(String);
|
||||
|
||||
/// {@template backboard_game_over}
|
||||
/// [PositionComponent] that handles the user input on the
|
||||
/// game over display view.
|
||||
/// {@endtemplate}
|
||||
class BackboardGameOver extends PositionComponent with HasGameRef {
|
||||
/// {@macro backboard_game_over}
|
||||
BackboardGameOver({
|
||||
required int score,
|
||||
required String characterIconPath,
|
||||
BackboardOnSubmit? onSubmit,
|
||||
}) : _onSubmit = onSubmit,
|
||||
super(
|
||||
children: [
|
||||
_BackboardSpriteComponent(),
|
||||
_BackboardDisplaySpriteComponent(),
|
||||
_ScoreTextComponent(score.formatScore()),
|
||||
_CharacterIconSpriteComponent(characterIconPath),
|
||||
],
|
||||
);
|
||||
|
||||
final BackboardOnSubmit? _onSubmit;
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
for (var i = 0; i < 3; i++) {
|
||||
await add(
|
||||
BackboardLetterPrompt(
|
||||
position: Vector2(
|
||||
24.3 + (4.5 * i),
|
||||
-45,
|
||||
),
|
||||
hasFocus: i == 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await add(
|
||||
KeyboardInputController(
|
||||
keyUp: {
|
||||
LogicalKeyboardKey.arrowLeft: () => _movePrompt(true),
|
||||
LogicalKeyboardKey.arrowRight: () => _movePrompt(false),
|
||||
LogicalKeyboardKey.enter: _submit,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the current inputed initials
|
||||
String get initials => children
|
||||
.whereType<BackboardLetterPrompt>()
|
||||
.map((prompt) => prompt.char)
|
||||
.join();
|
||||
|
||||
bool _submit() {
|
||||
_onSubmit?.call(initials);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _movePrompt(bool left) {
|
||||
final prompts = children.whereType<BackboardLetterPrompt>().toList();
|
||||
|
||||
final current = prompts.firstWhere((prompt) => prompt.hasFocus)
|
||||
..hasFocus = false;
|
||||
var index = prompts.indexOf(current) + (left ? -1 : 1);
|
||||
index = min(max(0, index), prompts.length - 1);
|
||||
|
||||
prompts[index].hasFocus = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class _BackboardSpriteComponent extends SpriteComponent with HasGameRef {
|
||||
_BackboardSpriteComponent() : super(anchor: Anchor.bottomCenter);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
final sprite = await gameRef.loadSprite(
|
||||
Assets.images.backboard.backboardGameOver.keyName,
|
||||
);
|
||||
this.sprite = sprite;
|
||||
size = sprite.originalSize / 10;
|
||||
}
|
||||
}
|
||||
|
||||
class _BackboardDisplaySpriteComponent extends SpriteComponent with HasGameRef {
|
||||
_BackboardDisplaySpriteComponent()
|
||||
: super(
|
||||
anchor: Anchor.bottomCenter,
|
||||
position: Vector2(0, -11.5),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
final sprite = await gameRef.loadSprite(
|
||||
Assets.images.backboard.display.keyName,
|
||||
);
|
||||
this.sprite = sprite;
|
||||
size = sprite.originalSize / 10;
|
||||
}
|
||||
}
|
||||
|
||||
class _ScoreTextComponent extends TextComponent {
|
||||
_ScoreTextComponent(String score)
|
||||
: super(
|
||||
text: score,
|
||||
anchor: Anchor.centerLeft,
|
||||
position: Vector2(-34, -45),
|
||||
textRenderer: Backboard.textPaint,
|
||||
);
|
||||
}
|
||||
|
||||
class _CharacterIconSpriteComponent extends SpriteComponent with HasGameRef {
|
||||
_CharacterIconSpriteComponent(String characterIconPath)
|
||||
: _characterIconPath = characterIconPath,
|
||||
super(
|
||||
anchor: Anchor.center,
|
||||
position: Vector2(18.4, -45),
|
||||
);
|
||||
|
||||
final String _characterIconPath;
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
final sprite = Sprite(gameRef.images.fromCache(_characterIconPath));
|
||||
this.sprite = sprite;
|
||||
size = sprite.originalSize / 10;
|
||||
}
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// {@template backboard_letter_prompt}
|
||||
/// A [PositionComponent] that renders a letter prompt used
|
||||
/// on the [BackboardGameOver]
|
||||
/// {@endtemplate}
|
||||
class BackboardLetterPrompt extends PositionComponent {
|
||||
/// {@macro backboard_letter_prompt}
|
||||
BackboardLetterPrompt({
|
||||
required Vector2 position,
|
||||
bool hasFocus = false,
|
||||
}) : _hasFocus = hasFocus,
|
||||
super(
|
||||
position: position,
|
||||
);
|
||||
|
||||
static const _alphabetCode = 65;
|
||||
static const _alphabetLength = 25;
|
||||
var _charIndex = 0;
|
||||
|
||||
bool _hasFocus;
|
||||
|
||||
late RectangleComponent _underscore;
|
||||
late TextComponent _input;
|
||||
late TimerComponent _underscoreBlinker;
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
_underscore = RectangleComponent(
|
||||
size: Vector2(3.8, 0.8),
|
||||
anchor: Anchor.center,
|
||||
position: Vector2(-0.3, 4),
|
||||
);
|
||||
|
||||
await add(_underscore);
|
||||
|
||||
_input = TextComponent(
|
||||
text: 'A',
|
||||
textRenderer: Backboard.textPaint,
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
await add(_input);
|
||||
|
||||
_underscoreBlinker = TimerComponent(
|
||||
period: 0.6,
|
||||
repeat: true,
|
||||
autoStart: _hasFocus,
|
||||
onTick: () {
|
||||
_underscore.paint.color = (_underscore.paint.color == Colors.white)
|
||||
? Colors.transparent
|
||||
: Colors.white;
|
||||
},
|
||||
);
|
||||
|
||||
await add(_underscoreBlinker);
|
||||
|
||||
await add(
|
||||
KeyboardInputController(
|
||||
keyUp: {
|
||||
LogicalKeyboardKey.arrowUp: () => _cycle(true),
|
||||
LogicalKeyboardKey.arrowDown: () => _cycle(false),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the current selected character
|
||||
String get char => String.fromCharCode(_alphabetCode + _charIndex);
|
||||
|
||||
bool _cycle(bool up) {
|
||||
if (_hasFocus) {
|
||||
final newCharCode =
|
||||
min(max(_charIndex + (up ? 1 : -1), 0), _alphabetLength);
|
||||
_input.text = String.fromCharCode(_alphabetCode + newCharCode);
|
||||
_charIndex = newCharCode;
|
||||
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Returns if this prompt has focus on it
|
||||
bool get hasFocus => _hasFocus;
|
||||
|
||||
/// Updates this prompt focus
|
||||
set hasFocus(bool hasFocus) {
|
||||
if (hasFocus) {
|
||||
_underscoreBlinker.timer.resume();
|
||||
} else {
|
||||
_underscoreBlinker.timer.pause();
|
||||
}
|
||||
_underscore.paint.color = Colors.white;
|
||||
_hasFocus = hasFocus;
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
/// [PositionComponent] that shows the leaderboard while the player
|
||||
/// has not started the game yet.
|
||||
class BackboardWaiting extends SpriteComponent with HasGameRef {
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
final sprite = await gameRef.loadSprite(
|
||||
Assets.images.backboard.backboardScores.keyName,
|
||||
);
|
||||
|
||||
this.sprite = sprite;
|
||||
size = sprite.originalSize / 10;
|
||||
anchor = Anchor.bottomCenter;
|
||||
}
|
||||
}
|
@ -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,92 @@
|
||||
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 bodyDef = BodyDef(
|
||||
position: initialPosition,
|
||||
type: BodyType.dynamic,
|
||||
userData: this,
|
||||
);
|
||||
|
||||
return world.createBody(bodyDef)..createFixtureFromShape(shape, 1);
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
import 'package:flame/input.dart';
|
||||
import 'package:pinball_components/pinball_components.dart' as components;
|
||||
import 'package:pinball_theme/pinball_theme.dart';
|
||||
import 'package:sandbox/common/common.dart';
|
||||
|
||||
class BackboardGameOverGame extends AssetsGame
|
||||
with HasKeyboardHandlerComponents {
|
||||
BackboardGameOverGame(this.score, this.character)
|
||||
: super(
|
||||
imagesFileNames: [
|
||||
components.Assets.images.score.fiveThousand.keyName,
|
||||
components.Assets.images.score.twentyThousand.keyName,
|
||||
components.Assets.images.score.twoHundredThousand.keyName,
|
||||
components.Assets.images.score.oneMillion.keyName,
|
||||
...characterIconPaths.values.toList(),
|
||||
],
|
||||
);
|
||||
|
||||
static const description = '''
|
||||
Shows how the Backboard in game over mode is rendered.
|
||||
|
||||
- Select a character to update the character icon.
|
||||
''';
|
||||
|
||||
static final characterIconPaths = <String, String>{
|
||||
'Dash': Assets.images.dash.leaderboardIcon.keyName,
|
||||
'Sparky': Assets.images.sparky.leaderboardIcon.keyName,
|
||||
'Android': Assets.images.android.leaderboardIcon.keyName,
|
||||
'Dino': Assets.images.dino.leaderboardIcon.keyName,
|
||||
};
|
||||
|
||||
final int score;
|
||||
|
||||
final String character;
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
camera
|
||||
..followVector2(Vector2.zero())
|
||||
..zoom = 5;
|
||||
|
||||
await add(
|
||||
components.Backboard.gameOver(
|
||||
position: Vector2(0, 20),
|
||||
score: score,
|
||||
characterIconPath: characterIconPaths[character]!,
|
||||
onSubmit: (initials) {
|
||||
add(
|
||||
components.ScoreComponent(
|
||||
points: components.Points.values
|
||||
.firstWhere((element) => element.value == score),
|
||||
position: Vector2(0, 50),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:sandbox/common/common.dart';
|
||||
|
||||
class BackboardWaitingGame extends AssetsGame {
|
||||
BackboardWaitingGame()
|
||||
: super(
|
||||
imagesFileNames: [],
|
||||
);
|
||||
|
||||
static const description = '''
|
||||
Shows how the Backboard in waiting mode is rendered.
|
||||
''';
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
camera
|
||||
..followVector2(Vector2.zero())
|
||||
..zoom = 5;
|
||||
|
||||
await add(
|
||||
Backboard.waiting(position: Vector2(0, 20)),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import 'package:dashbook/dashbook.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:sandbox/common/common.dart';
|
||||
import 'package:sandbox/stories/backboard/backboard_game_over_game.dart';
|
||||
import 'package:sandbox/stories/backboard/backboard_waiting_game.dart';
|
||||
|
||||
void addBackboardStories(Dashbook dashbook) {
|
||||
dashbook.storiesOf('Backboard')
|
||||
..addGame(
|
||||
title: 'Waiting',
|
||||
description: BackboardWaitingGame.description,
|
||||
gameBuilder: (_) => BackboardWaitingGame(),
|
||||
)
|
||||
..addGame(
|
||||
title: 'Game over',
|
||||
description: BackboardGameOverGame.description,
|
||||
gameBuilder: (context) => BackboardGameOverGame(
|
||||
context.listProperty(
|
||||
'Score',
|
||||
Points.values.first.value,
|
||||
Points.values.map((score) => score.value).toList(),
|
||||
),
|
||||
context.listProperty(
|
||||
'Character',
|
||||
BackboardGameOverGame.characterIconPaths.keys.first,
|
||||
BackboardGameOverGame.characterIconPaths.keys.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
@ -1,185 +0,0 @@
|
||||
// ignore_for_file: unawaited_futures, cascade_invocations
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball_components/pinball_components.dart' hide Assets;
|
||||
import 'package:pinball_theme/pinball_theme.dart';
|
||||
|
||||
import '../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('Backboard', () {
|
||||
final characterIconPath = Assets.images.dash.leaderboardIcon.keyName;
|
||||
final tester = FlameTester(() => KeyboardTestGame([characterIconPath]));
|
||||
|
||||
group('on waitingMode', () {
|
||||
tester.testGameWidget(
|
||||
'renders correctly',
|
||||
setUp: (game, tester) async {
|
||||
game.camera.zoom = 2;
|
||||
game.camera.followVector2(Vector2.zero());
|
||||
await game.ensureAdd(Backboard.waiting(position: Vector2(0, 15)));
|
||||
await tester.pump();
|
||||
},
|
||||
verify: (game, tester) async {
|
||||
await expectLater(
|
||||
find.byGame<TestGame>(),
|
||||
matchesGoldenFile('golden/backboard/waiting.png'),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('on gameOverMode', () {
|
||||
tester.testGameWidget(
|
||||
'renders correctly',
|
||||
setUp: (game, tester) async {
|
||||
game.camera.zoom = 2;
|
||||
game.camera.followVector2(Vector2.zero());
|
||||
final backboard = Backboard.gameOver(
|
||||
position: Vector2(0, 15),
|
||||
score: 1000,
|
||||
characterIconPath: characterIconPath,
|
||||
onSubmit: (_) {},
|
||||
);
|
||||
await game.ensureAdd(backboard);
|
||||
},
|
||||
verify: (game, tester) async {
|
||||
final prompts =
|
||||
game.descendants().whereType<BackboardLetterPrompt>().length;
|
||||
expect(prompts, equals(3));
|
||||
|
||||
final score = game.descendants().firstWhere(
|
||||
(component) =>
|
||||
component is TextComponent && component.text == '1,000',
|
||||
);
|
||||
expect(score, isNotNull);
|
||||
},
|
||||
);
|
||||
|
||||
tester.testGameWidget(
|
||||
'can change the initials',
|
||||
setUp: (game, tester) async {
|
||||
final backboard = Backboard.gameOver(
|
||||
position: Vector2(0, 15),
|
||||
score: 1000,
|
||||
characterIconPath: characterIconPath,
|
||||
onSubmit: (_) {},
|
||||
);
|
||||
await game.ensureAdd(backboard);
|
||||
|
||||
// Focus is already on the first letter
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.pump();
|
||||
|
||||
// Move to the next an press up again
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||||
await tester.pump();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.pump();
|
||||
|
||||
// One more time
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||||
await tester.pump();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.pump();
|
||||
|
||||
// Back to the previous and increase one more
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||||
await tester.pump();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.pump();
|
||||
},
|
||||
verify: (game, tester) async {
|
||||
final backboard = game
|
||||
.descendants()
|
||||
.firstWhere((component) => component is BackboardGameOver)
|
||||
as BackboardGameOver;
|
||||
|
||||
expect(backboard.initials, equals('BCB'));
|
||||
},
|
||||
);
|
||||
|
||||
String? submitedInitials;
|
||||
tester.testGameWidget(
|
||||
'submits the initials',
|
||||
setUp: (game, tester) async {
|
||||
final backboard = Backboard.gameOver(
|
||||
position: Vector2(0, 15),
|
||||
score: 1000,
|
||||
characterIconPath: characterIconPath,
|
||||
onSubmit: (value) {
|
||||
submitedInitials = value;
|
||||
},
|
||||
);
|
||||
await game.ensureAdd(backboard);
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pump();
|
||||
},
|
||||
verify: (game, tester) async {
|
||||
expect(submitedInitials, equals('AAA'));
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('BackboardLetterPrompt', () {
|
||||
final tester = FlameTester(KeyboardTestGame.new);
|
||||
|
||||
tester.testGameWidget(
|
||||
'cycles the char up and down when it has focus',
|
||||
setUp: (game, tester) async {
|
||||
await game.ensureAdd(
|
||||
BackboardLetterPrompt(hasFocus: true, position: Vector2.zero()),
|
||||
);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.pump();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.pump();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.pump();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.pump();
|
||||
},
|
||||
verify: (game, tester) async {
|
||||
final prompt = game.firstChild<BackboardLetterPrompt>();
|
||||
expect(prompt?.char, equals('C'));
|
||||
},
|
||||
);
|
||||
|
||||
tester.testGameWidget(
|
||||
"does nothing when it doesn't have focus",
|
||||
setUp: (game, tester) async {
|
||||
await game.ensureAdd(
|
||||
BackboardLetterPrompt(position: Vector2.zero()),
|
||||
);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.pump();
|
||||
},
|
||||
verify: (game, tester) async {
|
||||
final prompt = game.firstChild<BackboardLetterPrompt>();
|
||||
expect(prompt?.char, equals('A'));
|
||||
},
|
||||
);
|
||||
|
||||
tester.testGameWidget(
|
||||
'blinks the prompt when it has the focus',
|
||||
setUp: (game, tester) async {
|
||||
await game.ensureAdd(
|
||||
BackboardLetterPrompt(position: Vector2.zero(), hasFocus: true),
|
||||
);
|
||||
},
|
||||
verify: (game, tester) async {
|
||||
final underscore = game.descendants().whereType<ShapeComponent>().first;
|
||||
expect(underscore.paint.color, Colors.white);
|
||||
|
||||
game.update(2);
|
||||
expect(underscore.paint.color, Colors.transparent);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
// ignore_for_file: cascade_invocations
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
import '../../../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final asset = Assets.images.ball.ball.keyName;
|
||||
final flameTester = FlameTester(() => TestGame([asset]));
|
||||
|
||||
group('BallGravitatingBehavior', () {
|
||||
const baseColor = Color(0xFFFFFFFF);
|
||||
test('can be instantiated', () {
|
||||
expect(
|
||||
BallGravitatingBehavior(),
|
||||
isA<BallGravitatingBehavior>(),
|
||||
);
|
||||
});
|
||||
|
||||
flameTester.test('can be loaded', (game) async {
|
||||
final ball = Ball.test(baseColor: baseColor);
|
||||
final behavior = BallGravitatingBehavior();
|
||||
await ball.add(behavior);
|
||||
await game.ensureAdd(ball);
|
||||
expect(
|
||||
ball.firstChild<BallGravitatingBehavior>(),
|
||||
equals(behavior),
|
||||
);
|
||||
});
|
||||
|
||||
flameTester.test(
|
||||
"overrides the body's horizontal gravity symmetrically",
|
||||
(game) async {
|
||||
final ball1 = Ball.test(baseColor: baseColor)
|
||||
..initialPosition = Vector2(10, 0);
|
||||
await ball1.add(BallGravitatingBehavior());
|
||||
|
||||
final ball2 = Ball.test(baseColor: baseColor)
|
||||
..initialPosition = Vector2(-10, 0);
|
||||
await ball2.add(BallGravitatingBehavior());
|
||||
|
||||
await game.ensureAddAll([ball1, ball2]);
|
||||
game.update(1);
|
||||
|
||||
expect(
|
||||
ball1.body.gravityOverride!.x,
|
||||
equals(-ball2.body.gravityOverride!.x),
|
||||
);
|
||||
expect(
|
||||
ball1.body.gravityOverride!.y,
|
||||
equals(ball2.body.gravityOverride!.y),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
// ignore_for_file: cascade_invocations
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
import '../../../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final asset = Assets.images.ball.ball.keyName;
|
||||
final flameTester = FlameTester(() => TestGame([asset]));
|
||||
|
||||
group('BallScalingBehavior', () {
|
||||
const baseColor = Color(0xFFFFFFFF);
|
||||
test('can be instantiated', () {
|
||||
expect(
|
||||
BallScalingBehavior(),
|
||||
isA<BallScalingBehavior>(),
|
||||
);
|
||||
});
|
||||
|
||||
flameTester.test('can be loaded', (game) async {
|
||||
final ball = Ball.test(baseColor: baseColor);
|
||||
final behavior = BallScalingBehavior();
|
||||
await ball.add(behavior);
|
||||
await game.ensureAdd(ball);
|
||||
expect(
|
||||
ball.firstChild<BallScalingBehavior>(),
|
||||
equals(behavior),
|
||||
);
|
||||
});
|
||||
|
||||
flameTester.test('scales the shape radius', (game) async {
|
||||
final ball1 = Ball.test(baseColor: baseColor)
|
||||
..initialPosition = Vector2(0, 10);
|
||||
await ball1.add(BallScalingBehavior());
|
||||
|
||||
final ball2 = Ball.test(baseColor: baseColor)
|
||||
..initialPosition = Vector2(0, -10);
|
||||
await ball2.add(BallScalingBehavior());
|
||||
|
||||
await game.ensureAddAll([ball1, ball2]);
|
||||
game.update(1);
|
||||
|
||||
final shape1 = ball1.body.fixtures.first.shape;
|
||||
final shape2 = ball2.body.fixtures.first.shape;
|
||||
expect(
|
||||
shape1.radius,
|
||||
greaterThan(shape2.radius),
|
||||
);
|
||||
});
|
||||
|
||||
flameTester.test(
|
||||
'scales the sprite',
|
||||
(game) async {
|
||||
final ball1 = Ball.test(baseColor: baseColor)
|
||||
..initialPosition = Vector2(0, 10);
|
||||
await ball1.add(BallScalingBehavior());
|
||||
|
||||
final ball2 = Ball.test(baseColor: baseColor)
|
||||
..initialPosition = Vector2(0, -10);
|
||||
await ball2.add(BallScalingBehavior());
|
||||
|
||||
await game.ensureAddAll([ball1, ball2]);
|
||||
game.update(1);
|
||||
|
||||
final sprite1 = ball1.firstChild<SpriteComponent>()!;
|
||||
final sprite2 = ball2.firstChild<SpriteComponent>()!;
|
||||
expect(
|
||||
sprite1.scale.x,
|
||||
greaterThan(sprite2.scale.x),
|
||||
);
|
||||
expect(
|
||||
sprite1.scale.y,
|
||||
greaterThan(sprite2.scale.y),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
// ignore_for_file: cascade_invocations
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
import '../../../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group(
|
||||
'BallTurboChargingBehavior',
|
||||
() {
|
||||
final assets = [Assets.images.ball.ball.keyName];
|
||||
final flameTester = FlameTester(() => TestGame(assets));
|
||||
const baseColor = Color(0xFFFFFFFF);
|
||||
|
||||
test('can be instantiated', () {
|
||||
expect(
|
||||
BallTurboChargingBehavior(impulse: Vector2.zero()),
|
||||
isA<BallTurboChargingBehavior>(),
|
||||
);
|
||||
});
|
||||
|
||||
flameTester.test('can be loaded', (game) async {
|
||||
final ball = Ball.test(baseColor: baseColor);
|
||||
final behavior = BallTurboChargingBehavior(impulse: Vector2.zero());
|
||||
await ball.add(behavior);
|
||||
await game.ensureAdd(ball);
|
||||
expect(
|
||||
ball.firstChild<BallTurboChargingBehavior>(),
|
||||
equals(behavior),
|
||||
);
|
||||
});
|
||||
|
||||
flameTester.test(
|
||||
'impulses the ball velocity when loaded',
|
||||
(game) async {
|
||||
final ball = Ball.test(baseColor: baseColor);
|
||||
await game.ensureAdd(ball);
|
||||
final impulse = Vector2.all(1);
|
||||
final behavior = BallTurboChargingBehavior(impulse: impulse);
|
||||
await ball.ensureAdd(behavior);
|
||||
|
||||
expect(
|
||||
ball.body.linearVelocity.x,
|
||||
equals(impulse.x),
|
||||
);
|
||||
expect(
|
||||
ball.body.linearVelocity.y,
|
||||
equals(impulse.y),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
flameTester.test('adds sprite', (game) async {
|
||||
final ball = Ball(baseColor: baseColor);
|
||||
await game.ensureAdd(ball);
|
||||
|
||||
await ball.ensureAdd(
|
||||
BallTurboChargingBehavior(impulse: Vector2.zero()),
|
||||
);
|
||||
|
||||
expect(
|
||||
ball.children.whereType<SpriteAnimationComponent>().single,
|
||||
isNotNull,
|
||||
);
|
||||
});
|
||||
|
||||
flameTester.test('removes sprite after it finishes', (game) async {
|
||||
final ball = Ball(baseColor: baseColor);
|
||||
await game.ensureAdd(ball);
|
||||
|
||||
final behavior = BallTurboChargingBehavior(impulse: Vector2.zero());
|
||||
await ball.ensureAdd(behavior);
|
||||
|
||||
final turboChargeSpriteAnimation =
|
||||
ball.children.whereType<SpriteAnimationComponent>().single;
|
||||
|
||||
expect(ball.contains(turboChargeSpriteAnimation), isTrue);
|
||||
|
||||
game.update(behavior.timer.limit);
|
||||
game.update(0.1);
|
||||
|
||||
expect(ball.contains(turboChargeSpriteAnimation), isFalse);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
// ignore_for_file: cascade_invocations
|
||||
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_components/src/components/flapper/behaviors/behaviors.dart';
|
||||
|
||||
import '../../../../helpers/helpers.dart';
|
||||
|
||||
class _MockBall extends Mock implements Ball {}
|
||||
|
||||
class _MockContact extends Mock implements Contact {}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final assets = [
|
||||
Assets.images.flapper.flap.keyName,
|
||||
Assets.images.flapper.backSupport.keyName,
|
||||
Assets.images.flapper.frontSupport.keyName,
|
||||
];
|
||||
final flameTester = FlameTester(() => TestGame(assets));
|
||||
|
||||
group(
|
||||
'FlapperSpinningBehavior',
|
||||
() {
|
||||
test('can be instantiated', () {
|
||||
expect(
|
||||
FlapperSpinningBehavior(),
|
||||
isA<FlapperSpinningBehavior>(),
|
||||
);
|
||||
});
|
||||
|
||||
flameTester.test(
|
||||
'beginContact plays the flapper animation',
|
||||
(game) async {
|
||||
final behavior = FlapperSpinningBehavior();
|
||||
final entrance = FlapperEntrance();
|
||||
final flap = FlapSpriteAnimationComponent();
|
||||
final flapper = Flapper.test();
|
||||
await flapper.addAll([entrance, flap]);
|
||||
await entrance.add(behavior);
|
||||
await game.ensureAdd(flapper);
|
||||
|
||||
behavior.beginContact(_MockBall(), _MockContact());
|
||||
|
||||
expect(flap.playing, isTrue);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
// ignore_for_file: cascade_invocations
|
||||
|
||||
import 'package:flame_forge2d/flame_forge2d.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_components/src/components/flapper/behaviors/behaviors.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
import '../../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('Flapper', () {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final assets = [
|
||||
Assets.images.flapper.flap.keyName,
|
||||
Assets.images.flapper.backSupport.keyName,
|
||||
Assets.images.flapper.frontSupport.keyName,
|
||||
];
|
||||
final flameTester = FlameTester(() => TestGame(assets));
|
||||
|
||||
flameTester.test('loads correctly', (game) async {
|
||||
final component = Flapper();
|
||||
await game.ensureAdd(component);
|
||||
expect(game.contains(component), isTrue);
|
||||
});
|
||||
|
||||
flameTester.testGameWidget(
|
||||
'renders correctly',
|
||||
setUp: (game, tester) async {
|
||||
await game.images.loadAll(assets);
|
||||
final canvas = ZCanvasComponent(children: [Flapper()]);
|
||||
await game.ensureAdd(canvas);
|
||||
game.camera
|
||||
..followVector2(Vector2(3, -70))
|
||||
..zoom = 25;
|
||||
await tester.pump();
|
||||
},
|
||||
verify: (game, tester) async {
|
||||
const goldenFilePath = '../golden/flapper/';
|
||||
final flapSpriteAnimationComponent = game
|
||||
.descendants()
|
||||
.whereType<FlapSpriteAnimationComponent>()
|
||||
.first
|
||||
..playing = true;
|
||||
final animationDuration =
|
||||
flapSpriteAnimationComponent.animation!.totalDuration();
|
||||
|
||||
await expectLater(
|
||||
find.byGame<TestGame>(),
|
||||
matchesGoldenFile('${goldenFilePath}start.png'),
|
||||
);
|
||||
|
||||
game.update(animationDuration * 0.25);
|
||||
await tester.pump();
|
||||
await expectLater(
|
||||
find.byGame<TestGame>(),
|
||||
matchesGoldenFile('${goldenFilePath}middle.png'),
|
||||
);
|
||||
|
||||
game.update(animationDuration * 0.75);
|
||||
await tester.pump();
|
||||
await expectLater(
|
||||
find.byGame<TestGame>(),
|
||||
matchesGoldenFile('${goldenFilePath}end.png'),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
flameTester.test('adds a FlapperSpiningBehavior to FlapperEntrance',
|
||||
(game) async {
|
||||
final flapper = Flapper();
|
||||
await game.ensureAdd(flapper);
|
||||
|
||||
final flapperEntrance = flapper.firstChild<FlapperEntrance>()!;
|
||||
expect(
|
||||
flapperEntrance.firstChild<FlapperSpinningBehavior>(),
|
||||
isNotNull,
|
||||
);
|
||||
});
|
||||
|
||||
flameTester.test(
|
||||
'flap stops animating after animation completes',
|
||||
(game) async {
|
||||
final flapper = Flapper();
|
||||
await game.ensureAdd(flapper);
|
||||
|
||||
final flapSpriteAnimationComponent =
|
||||
flapper.firstChild<FlapSpriteAnimationComponent>()!;
|
||||
|
||||
flapSpriteAnimationComponent.playing = true;
|
||||
game.update(
|
||||
flapSpriteAnimationComponent.animation!.totalDuration() + 0.1,
|
||||
);
|
||||
|
||||
expect(flapSpriteAnimationComponent.playing, isFalse);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
Before Width: | Height: | Size: 844 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 80 KiB |
@ -0,0 +1,10 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
void main() {
|
||||
group('ScoreX', () {
|
||||
test('formatScore correctly formats int', () {
|
||||
expect(1000000.formatScore(), '1,000,000');
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:flame_forge2d/world_contact_listener.dart';
|
||||
|
||||
// NOTE(wolfen): This should be removed when https://github.com/flame-engine/flame/pull/1597 is solved.
|
||||
/// {@template pinball_forge2d_game}
|
||||
/// A [Game] that uses the Forge2D physics engine.
|
||||
/// {@endtemplate}
|
||||
class PinballForge2DGame extends FlameGame implements Forge2DGame {
|
||||
/// {@macro pinball_forge2d_game}
|
||||
PinballForge2DGame({
|
||||
required Vector2 gravity,
|
||||
}) : world = World(gravity),
|
||||
super(camera: Camera()) {
|
||||
camera.zoom = Forge2DGame.defaultZoom;
|
||||
world.setContactListener(WorldContactListener());
|
||||
}
|
||||
|
||||
@override
|
||||
final World world;
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
world.stepDt(min(dt, 1 / 60));
|
||||
}
|
||||
|
||||
@override
|
||||
Vector2 screenToFlameWorld(Vector2 position) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Vector2 screenToWorld(Vector2 position) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Vector2 worldToScreen(Vector2 position) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
// ignore_for_file: cascade_invocations
|
||||
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
void main() {
|
||||
final flameTester = FlameTester(
|
||||
() => PinballForge2DGame(gravity: Vector2.zero()),
|
||||
);
|
||||
|
||||
group('PinballForge2DGame', () {
|
||||
test('can instantiate', () {
|
||||
expect(
|
||||
() => PinballForge2DGame(gravity: Vector2.zero()),
|
||||
returnsNormally,
|
||||
);
|
||||
});
|
||||
|
||||
flameTester.test(
|
||||
'screenToFlameWorld throws UnimpelementedError',
|
||||
(game) async {
|
||||
expect(
|
||||
() => game.screenToFlameWorld(Vector2.zero()),
|
||||
throwsUnimplementedError,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
flameTester.test(
|
||||
'screenToWorld throws UnimpelementedError',
|
||||
(game) async {
|
||||
expect(
|
||||
() => game.screenToWorld(Vector2.zero()),
|
||||
throwsUnimplementedError,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
flameTester.test(
|
||||
'worldToScreen throws UnimpelementedError',
|
||||
(game) async {
|
||||
expect(
|
||||
() => game.worldToScreen(Vector2.zero()),
|
||||
throwsUnimplementedError,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|