@ -1,11 +1,33 @@
|
||||
{
|
||||
"firestore": {
|
||||
"rules": "firestore.rules"
|
||||
},
|
||||
"hosting": {
|
||||
"public": "build/web",
|
||||
"site": "ashehwkdkdjruejdnensjsjdne",
|
||||
"ignore": [
|
||||
"firebase.json",
|
||||
"**/.*",
|
||||
"**/node_modules/**"
|
||||
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
||||
"headers": [
|
||||
{
|
||||
"source": "**/*.@(jpg|jpeg|gif|png)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "max-age=3600"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "**",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "no-cache, no-store, must-revalidate"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"storage": {
|
||||
"rules": "storage.rules"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
rules_version = '2';
|
||||
service cloud.firestore {
|
||||
match /databases/{database}/documents {
|
||||
match /leaderboard/{userId} {
|
||||
|
||||
function prohibited(initials) {
|
||||
let prohibitedInitials = get(/databases/$(database)/documents/prohibitedInitials/list).data.prohibitedInitials;
|
||||
return initials in prohibitedInitials;
|
||||
}
|
||||
|
||||
function inCharLimit(initials) {
|
||||
return initials.size() < 4;
|
||||
}
|
||||
|
||||
function isAuthedUser(auth) {
|
||||
return request.auth.uid != null && auth.token.firebase.sign_in_provider == "anonymous"
|
||||
}
|
||||
|
||||
// Leaderboard can be read if it doesn't contain any prohibited initials
|
||||
allow read: if !prohibited(resource.data.playerInitials);
|
||||
|
||||
// A leaderboard entry can be created if the user is authenticated,
|
||||
// it's 3 characters long, and not a prohibited combination.
|
||||
allow create: if isAuthedUser(request.auth) &&
|
||||
inCharLimit(request.resource.data.playerInitials) &&
|
||||
!prohibited(request.resource.data.playerInitials);
|
||||
}
|
||||
}
|
||||
}
|
@ -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,78 @@
|
||||
// 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));
|
||||
final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single;
|
||||
await canvas.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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1 +1,3 @@
|
||||
export 'android_spaceship_bonus_behavior.dart';
|
||||
export 'ramp_bonus_behavior.dart';
|
||||
export 'ramp_shot_behavior.dart';
|
||||
|
@ -0,0 +1,62 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinball/game/behaviors/behaviors.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// {@template ramp_bonus_behavior}
|
||||
/// Increases the score when a [Ball] is shot 10 times into the [SpaceshipRamp].
|
||||
/// {@endtemplate}
|
||||
class RampBonusBehavior extends Component
|
||||
with ParentIsA<SpaceshipRamp>, HasGameRef<PinballGame> {
|
||||
/// {@macro ramp_bonus_behavior}
|
||||
RampBonusBehavior({
|
||||
required Points points,
|
||||
}) : _points = points,
|
||||
super();
|
||||
|
||||
/// Creates a [RampBonusBehavior].
|
||||
///
|
||||
/// This can be used for testing [RampBonusBehavior] in isolation.
|
||||
@visibleForTesting
|
||||
RampBonusBehavior.test({
|
||||
required Points points,
|
||||
required this.subscription,
|
||||
}) : _points = points,
|
||||
super();
|
||||
|
||||
final Points _points;
|
||||
|
||||
/// Subscription to [SpaceshipRampState] at [SpaceshipRamp].
|
||||
@visibleForTesting
|
||||
StreamSubscription? subscription;
|
||||
|
||||
@override
|
||||
void onMount() {
|
||||
super.onMount();
|
||||
|
||||
subscription = subscription ??
|
||||
parent.bloc.stream.listen((state) {
|
||||
final achievedOneMillionPoints = state.hits % 10 == 0;
|
||||
|
||||
if (achievedOneMillionPoints) {
|
||||
parent.add(
|
||||
ScoringBehavior(
|
||||
points: _points,
|
||||
position: Vector2(0, -60),
|
||||
duration: 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onRemove() {
|
||||
subscription?.cancel();
|
||||
super.onRemove();
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:pinball/game/behaviors/behaviors.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// {@template ramp_shot_behavior}
|
||||
/// Increases the score when a [Ball] is shot into the [SpaceshipRamp].
|
||||
/// {@endtemplate}
|
||||
class RampShotBehavior extends Component
|
||||
with ParentIsA<SpaceshipRamp>, HasGameRef<PinballGame> {
|
||||
/// {@macro ramp_shot_behavior}
|
||||
RampShotBehavior({
|
||||
required Points points,
|
||||
}) : _points = points,
|
||||
super();
|
||||
|
||||
/// Creates a [RampShotBehavior].
|
||||
///
|
||||
/// This can be used for testing [RampShotBehavior] in isolation.
|
||||
@visibleForTesting
|
||||
RampShotBehavior.test({
|
||||
required Points points,
|
||||
required this.subscription,
|
||||
}) : _points = points,
|
||||
super();
|
||||
|
||||
final Points _points;
|
||||
|
||||
/// Subscription to [SpaceshipRampState] at [SpaceshipRamp].
|
||||
@visibleForTesting
|
||||
StreamSubscription? subscription;
|
||||
|
||||
@override
|
||||
void onMount() {
|
||||
super.onMount();
|
||||
|
||||
subscription = subscription ??
|
||||
parent.bloc.stream.listen((state) {
|
||||
final achievedOneMillionPoints = state.hits % 10 == 0;
|
||||
|
||||
if (!achievedOneMillionPoints) {
|
||||
gameRef.read<GameBloc>().add(const MultiplierIncreased());
|
||||
|
||||
parent.add(
|
||||
ScoringBehavior(
|
||||
points: _points,
|
||||
position: Vector2(0, -45),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onRemove() {
|
||||
subscription?.cancel();
|
||||
super.onRemove();
|
||||
}
|
||||
}
|
@ -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,53 +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.firstChild<ZCanvasComponent>()!.add(
|
||||
ScoreComponent(
|
||||
points: _points,
|
||||
position: other.body.position,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// {@template bumper_scoring_behavior}
|
||||
/// A specific [ScoringBehavior] used for Bumpers.
|
||||
/// In addition to its parent logic, also plays the
|
||||
/// SFX for bumpers
|
||||
/// {@endtemplate}
|
||||
class BumperScoringBehavior extends ScoringBehavior {
|
||||
/// {@macro bumper_scoring_behavior}
|
||||
BumperScoringBehavior({
|
||||
required Points points,
|
||||
}) : super(points: points);
|
||||
|
||||
@override
|
||||
void beginContact(Object other, Contact contact) {
|
||||
super.beginContact(other, contact);
|
||||
if (other is! Ball) return;
|
||||
|
||||
gameRef.audio.bumper();
|
||||
}
|
||||
}
|
@ -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: 616 KiB After Width: | Height: | Size: 636 KiB |
Before Width: | Height: | Size: 735 KiB After Width: | Height: | Size: 259 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 |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 886 KiB After Width: | Height: | Size: 1012 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 94 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 569 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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
export 'ball_gravitating_behavior.dart';
|
||||
export 'ball_scaling_behavior.dart';
|
||||
export 'ball_turbo_charging_behavior.dart';
|
||||
|
@ -1,90 +0,0 @@
|
||||
// ignore_for_file: avoid_renaming_method_parameters
|
||||
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
/// {@template layer_entrance_orientation}
|
||||
/// Determines if a layer entrance is oriented [up] or [down] on the board.
|
||||
/// {@endtemplate}
|
||||
enum LayerEntranceOrientation {
|
||||
/// Facing up on the Board.
|
||||
up,
|
||||
|
||||
/// Facing down on the Board.
|
||||
down,
|
||||
}
|
||||
|
||||
/// {@template layer_sensor}
|
||||
/// [BodyComponent] located at the entrance and exit of a [Layer].
|
||||
///
|
||||
/// By default the base [layer] is set to [Layer.board] and the
|
||||
/// [_outsideZIndex] is set to [ZIndexes.ballOnBoard].
|
||||
/// {@endtemplate}
|
||||
abstract class LayerSensor extends BodyComponent
|
||||
with InitialPosition, Layered, ContactCallbacks {
|
||||
/// {@macro layer_sensor}
|
||||
LayerSensor({
|
||||
required Layer insideLayer,
|
||||
Layer? outsideLayer,
|
||||
required int insideZIndex,
|
||||
int? outsideZIndex,
|
||||
required this.orientation,
|
||||
}) : _insideLayer = insideLayer,
|
||||
_outsideLayer = outsideLayer ?? Layer.board,
|
||||
_insideZIndex = insideZIndex,
|
||||
_outsideZIndex = outsideZIndex ?? ZIndexes.ballOnBoard,
|
||||
super(renderBody: false) {
|
||||
layer = Layer.opening;
|
||||
}
|
||||
|
||||
final Layer _insideLayer;
|
||||
final Layer _outsideLayer;
|
||||
final int _insideZIndex;
|
||||
final int _outsideZIndex;
|
||||
|
||||
/// The [Shape] of the [LayerSensor].
|
||||
Shape get shape;
|
||||
|
||||
/// {@macro layer_entrance_orientation}
|
||||
// TODO(ruimiguel): Try to remove the need of [LayerEntranceOrientation] for
|
||||
// collision calculations.
|
||||
final LayerEntranceOrientation orientation;
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final fixtureDef = FixtureDef(
|
||||
shape,
|
||||
isSensor: true,
|
||||
);
|
||||
final bodyDef = BodyDef(
|
||||
position: initialPosition,
|
||||
userData: this,
|
||||
);
|
||||
|
||||
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||
}
|
||||
|
||||
@override
|
||||
void beginContact(Object other, Contact contact) {
|
||||
super.beginContact(other, contact);
|
||||
if (other is! Ball) return;
|
||||
|
||||
if (other.layer != _insideLayer) {
|
||||
final isBallEnteringOpening =
|
||||
(orientation == LayerEntranceOrientation.down &&
|
||||
other.body.linearVelocity.y < 0) ||
|
||||
(orientation == LayerEntranceOrientation.up &&
|
||||
other.body.linearVelocity.y > 0);
|
||||
|
||||
if (isBallEnteringOpening) {
|
||||
other
|
||||
..layer = _insideLayer
|
||||
..zIndex = _insideZIndex;
|
||||
}
|
||||
} else {
|
||||
other
|
||||
..layer = _outsideLayer
|
||||
..zIndex = _outsideZIndex;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export 'behaviors.dart';
|
||||
export 'layer_filtering_behavior.dart';
|
@ -0,0 +1,31 @@
|
||||
// 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 LayerFilteringBehavior extends ContactBehavior<LayerSensor> {
|
||||
@override
|
||||
void beginContact(Object other, Contact contact) {
|
||||
super.beginContact(other, contact);
|
||||
if (other is! Ball) return;
|
||||
|
||||
if (other.layer != parent.insideLayer) {
|
||||
final isBallEnteringOpening =
|
||||
(parent.orientation == LayerEntranceOrientation.down &&
|
||||
other.body.linearVelocity.y < 0) ||
|
||||
(parent.orientation == LayerEntranceOrientation.up &&
|
||||
other.body.linearVelocity.y > 0);
|
||||
|
||||
if (isBallEnteringOpening) {
|
||||
other
|
||||
..layer = parent.insideLayer
|
||||
..zIndex = parent.insideZIndex;
|
||||
}
|
||||
} else {
|
||||
other
|
||||
..layer = parent.outsideLayer
|
||||
..zIndex = parent.outsideZIndex;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
// ignore_for_file: avoid_renaming_method_parameters, public_member_api_docs
|
||||
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_components/src/components/layer_sensor/behaviors/layer_filtering_behavior.dart';
|
||||
|
||||
/// {@template layer_entrance_orientation}
|
||||
/// Determines if a layer entrance is oriented [up] or [down] on the board.
|
||||
/// {@endtemplate}
|
||||
enum LayerEntranceOrientation {
|
||||
/// Facing up on the Board.
|
||||
up,
|
||||
|
||||
/// Facing down on the Board.
|
||||
down,
|
||||
}
|
||||
|
||||
/// {@template layer_sensor}
|
||||
/// [BodyComponent] located at the entrance and exit of a [Layer].
|
||||
///
|
||||
/// By default the base [layer] is set to [Layer.board] and the
|
||||
/// [outsideZIndex] is set to [ZIndexes.ballOnBoard].
|
||||
/// {@endtemplate}
|
||||
abstract class LayerSensor extends BodyComponent with InitialPosition, Layered {
|
||||
/// {@macro layer_sensor}
|
||||
LayerSensor({
|
||||
required this.insideLayer,
|
||||
Layer? outsideLayer,
|
||||
required this.insideZIndex,
|
||||
int? outsideZIndex,
|
||||
required this.orientation,
|
||||
}) : outsideLayer = outsideLayer ?? Layer.board,
|
||||
outsideZIndex = outsideZIndex ?? ZIndexes.ballOnBoard,
|
||||
super(
|
||||
renderBody: false,
|
||||
children: [LayerFilteringBehavior()],
|
||||
) {
|
||||
layer = Layer.opening;
|
||||
}
|
||||
|
||||
final Layer insideLayer;
|
||||
|
||||
final Layer outsideLayer;
|
||||
|
||||
final int insideZIndex;
|
||||
|
||||
final int outsideZIndex;
|
||||
|
||||
/// The [Shape] of the [LayerSensor].
|
||||
Shape get shape;
|
||||
|
||||
/// {@macro layer_entrance_orientation}
|
||||
// TODO(ruimiguel): Try to remove the need of [LayerEntranceOrientation] for
|
||||
// collision calculations.
|
||||
final LayerEntranceOrientation orientation;
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final fixtureDef = FixtureDef(
|
||||
shape,
|
||||
isSensor: true,
|
||||
);
|
||||
final bodyDef = BodyDef(position: initialPosition);
|
||||
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export 'skill_shot_ball_contact_behavior.dart';
|
||||
export 'skill_shot_blinking_behavior.dart';
|
@ -0,0 +1,16 @@
|
||||
// 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 SkillShotBallContactBehavior extends ContactBehavior<SkillShot> {
|
||||
@override
|
||||
void beginContact(Object other, Contact contact) {
|
||||
super.beginContact(other, contact);
|
||||
if (other is! Ball) return;
|
||||
parent.bloc.onBallContacted();
|
||||
parent.firstChild<SpriteAnimationComponent>()?.playing = true;
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// {@template skill_shot_blinking_behavior}
|
||||
/// Makes a [SkillShot] blink between [SkillShotSpriteState.lit] and
|
||||
/// [SkillShotSpriteState.dimmed] for a set amount of blinks.
|
||||
/// {@endtemplate}
|
||||
class SkillShotBlinkingBehavior extends TimerComponent
|
||||
with ParentIsA<SkillShot> {
|
||||
/// {@macro skill_shot_blinking_behavior}
|
||||
SkillShotBlinkingBehavior() : super(period: 0.15);
|
||||
|
||||
final _maxBlinks = 4;
|
||||
int _blinks = 0;
|
||||
|
||||
void _onNewState(SkillShotState state) {
|
||||
if (state.isBlinking) {
|
||||
timer
|
||||
..reset()
|
||||
..start();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
timer.stop();
|
||||
parent.bloc.stream.listen(_onNewState);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTick() {
|
||||
super.onTick();
|
||||
if (_blinks != _maxBlinks * 2) {
|
||||
parent.bloc.switched();
|
||||
_blinks++;
|
||||
} else {
|
||||
_blinks = 0;
|
||||
timer.stop();
|
||||
parent.bloc.onBlinkingFinished();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
part 'skill_shot_state.dart';
|
||||
|
||||
class SkillShotCubit extends Cubit<SkillShotState> {
|
||||
SkillShotCubit() : super(const SkillShotState.initial());
|
||||
|
||||
void onBallContacted() {
|
||||
emit(
|
||||
const SkillShotState(
|
||||
spriteState: SkillShotSpriteState.lit,
|
||||
isBlinking: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void switched() {
|
||||
switch (state.spriteState) {
|
||||
case SkillShotSpriteState.lit:
|
||||
emit(state.copyWith(spriteState: SkillShotSpriteState.dimmed));
|
||||
break;
|
||||
case SkillShotSpriteState.dimmed:
|
||||
emit(state.copyWith(spriteState: SkillShotSpriteState.lit));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void onBlinkingFinished() {
|
||||
emit(
|
||||
const SkillShotState(
|
||||
spriteState: SkillShotSpriteState.dimmed,
|
||||
isBlinking: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
part of 'skill_shot_cubit.dart';
|
||||
|
||||
enum SkillShotSpriteState {
|
||||
lit,
|
||||
dimmed,
|
||||
}
|
||||
|
||||
class SkillShotState extends Equatable {
|
||||
const SkillShotState({
|
||||
required this.spriteState,
|
||||
required this.isBlinking,
|
||||
});
|
||||
|
||||
const SkillShotState.initial()
|
||||
: this(
|
||||
spriteState: SkillShotSpriteState.dimmed,
|
||||
isBlinking: false,
|
||||
);
|
||||
|
||||
final SkillShotSpriteState spriteState;
|
||||
|
||||
final bool isBlinking;
|
||||
|
||||
SkillShotState copyWith({
|
||||
SkillShotSpriteState? spriteState,
|
||||
bool? isBlinking,
|
||||
}) =>
|
||||
SkillShotState(
|
||||
spriteState: spriteState ?? this.spriteState,
|
||||
isBlinking: isBlinking ?? this.isBlinking,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [spriteState, isBlinking];
|
||||
}
|
@ -0,0 +1,169 @@
|
||||
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/skill_shot/behaviors/behaviors.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
export 'cubit/skill_shot_cubit.dart';
|
||||
|
||||
/// {@template skill_shot}
|
||||
/// Rollover awarding extra points.
|
||||
/// {@endtemplate}
|
||||
class SkillShot extends BodyComponent with ZIndex {
|
||||
/// {@macro skill_shot}
|
||||
SkillShot({Iterable<Component>? children})
|
||||
: this._(
|
||||
children: children,
|
||||
bloc: SkillShotCubit(),
|
||||
);
|
||||
|
||||
SkillShot._({
|
||||
Iterable<Component>? children,
|
||||
required this.bloc,
|
||||
}) : super(
|
||||
renderBody: false,
|
||||
children: [
|
||||
SkillShotBallContactBehavior(),
|
||||
SkillShotBlinkingBehavior(),
|
||||
_RolloverDecalSpriteComponent(),
|
||||
PinSpriteAnimationComponent(),
|
||||
_TextDecalSpriteGroupComponent(state: bloc.state.spriteState),
|
||||
...?children,
|
||||
],
|
||||
) {
|
||||
zIndex = ZIndexes.decal;
|
||||
}
|
||||
|
||||
/// Creates a [SkillShot] without any children.
|
||||
///
|
||||
/// This can be used for testing [SkillShot]'s behaviors in isolation.
|
||||
// TODO(alestiago): Refactor injecting bloc once the following is merged:
|
||||
// https://github.com/flame-engine/flame/pull/1538
|
||||
@visibleForTesting
|
||||
SkillShot.test({
|
||||
required this.bloc,
|
||||
});
|
||||
|
||||
// TODO(alestiago): Consider refactoring once the following is merged:
|
||||
// https://github.com/flame-engine/flame/pull/1538
|
||||
// ignore: public_member_api_docs
|
||||
final SkillShotCubit bloc;
|
||||
|
||||
@override
|
||||
void onRemove() {
|
||||
bloc.close();
|
||||
super.onRemove();
|
||||
}
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final shape = PolygonShape()
|
||||
..setAsBox(
|
||||
0.1,
|
||||
3.7,
|
||||
Vector2(-31.9, 9.1),
|
||||
0.11,
|
||||
);
|
||||
final fixtureDef = FixtureDef(shape, isSensor: true);
|
||||
return world.createBody(BodyDef())..createFixture(fixtureDef);
|
||||
}
|
||||
}
|
||||
|
||||
class _RolloverDecalSpriteComponent extends SpriteComponent with HasGameRef {
|
||||
_RolloverDecalSpriteComponent()
|
||||
: super(
|
||||
anchor: Anchor.center,
|
||||
position: Vector2(-31.9, 9.1),
|
||||
angle: 0.11,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
final sprite = Sprite(
|
||||
gameRef.images.fromCache(
|
||||
Assets.images.skillShot.decal.keyName,
|
||||
),
|
||||
);
|
||||
this.sprite = sprite;
|
||||
size = sprite.originalSize / 20;
|
||||
}
|
||||
}
|
||||
|
||||
/// {@template pin_sprite_animation_component}
|
||||
/// Animation for pin in [SkillShot] rollover.
|
||||
/// {@endtemplate}
|
||||
@visibleForTesting
|
||||
class PinSpriteAnimationComponent extends SpriteAnimationComponent
|
||||
with HasGameRef {
|
||||
/// {@macro pin_sprite_animation_component}
|
||||
PinSpriteAnimationComponent()
|
||||
: super(
|
||||
anchor: Anchor.center,
|
||||
position: Vector2(-31.9, 9.1),
|
||||
angle: 0,
|
||||
playing: false,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
final spriteSheet = gameRef.images.fromCache(
|
||||
Assets.images.skillShot.pin.keyName,
|
||||
);
|
||||
|
||||
const amountPerRow = 3;
|
||||
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 _TextDecalSpriteGroupComponent
|
||||
extends SpriteGroupComponent<SkillShotSpriteState>
|
||||
with HasGameRef, ParentIsA<SkillShot> {
|
||||
_TextDecalSpriteGroupComponent({
|
||||
required SkillShotSpriteState state,
|
||||
}) : super(
|
||||
anchor: Anchor.center,
|
||||
position: Vector2(-35.55, 3.59),
|
||||
current: state,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
parent.bloc.stream.listen((state) => current = state.spriteState);
|
||||
|
||||
final sprites = {
|
||||
SkillShotSpriteState.lit: Sprite(
|
||||
gameRef.images.fromCache(Assets.images.skillShot.lit.keyName),
|
||||
),
|
||||
SkillShotSpriteState.dimmed: Sprite(
|
||||
gameRef.images.fromCache(Assets.images.skillShot.dimmed.keyName),
|
||||
),
|
||||
};
|
||||
this.sprites = sprites;
|
||||
size = sprites[current]!.originalSize / 10;
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export 'ramp_ball_ascending_contact_behavior.dart';
|
@ -0,0 +1,24 @@
|
||||
// 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';
|
||||
|
||||
/// {@template ramp_ball_ascending_contact_behavior}
|
||||
/// Detects an ascending [Ball] that enters into the [SpaceshipRamp].
|
||||
///
|
||||
/// The [Ball] can hit with sensor to recognize if a [Ball] goes into or out of
|
||||
/// the [SpaceshipRamp].
|
||||
/// {@endtemplate}
|
||||
class RampBallAscendingContactBehavior
|
||||
extends ContactBehavior<RampScoringSensor> {
|
||||
@override
|
||||
void beginContact(Object other, Contact contact) {
|
||||
super.beginContact(other, contact);
|
||||
if (other is! Ball) return;
|
||||
|
||||
if (other.body.linearVelocity.y < 0) {
|
||||
parent.parent.bloc.onAscendingBallEntered();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
part 'spaceship_ramp_state.dart';
|
||||
|
||||
class SpaceshipRampCubit extends Cubit<SpaceshipRampState> {
|
||||
SpaceshipRampCubit() : super(const SpaceshipRampState.initial());
|
||||
|
||||
void onAscendingBallEntered() {
|
||||
emit(
|
||||
state.copyWith(hits: state.hits + 1),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
part of 'spaceship_ramp_cubit.dart';
|
||||
|
||||
class SpaceshipRampState extends Equatable {
|
||||
const SpaceshipRampState({
|
||||
required this.hits,
|
||||
}) : assert(hits >= 0, "Hits can't be negative");
|
||||
|
||||
const SpaceshipRampState.initial() : this(hits: 0);
|
||||
|
||||
final int hits;
|
||||
|
||||
SpaceshipRampState copyWith({
|
||||
int? hits,
|
||||
}) {
|
||||
return SpaceshipRampState(
|
||||
hits: hits ?? this.hits,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [hits];
|
||||
}
|