@ -1,11 +1,33 @@
|
|||||||
{
|
{
|
||||||
|
"firestore": {
|
||||||
|
"rules": "firestore.rules"
|
||||||
|
},
|
||||||
"hosting": {
|
"hosting": {
|
||||||
"public": "build/web",
|
"public": "build/web",
|
||||||
"site": "ashehwkdkdjruejdnensjsjdne",
|
"site": "ashehwkdkdjruejdnensjsjdne",
|
||||||
"ignore": [
|
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
||||||
"firebase.json",
|
"headers": [
|
||||||
"**/.*",
|
{
|
||||||
"**/node_modules/**"
|
"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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1,3 @@
|
|||||||
export 'android_spaceship_bonus_behavior.dart';
|
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,33 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_bloc/flame_bloc.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_audio/pinball_audio.dart';
|
||||||
|
|
||||||
|
/// Listens to the [GameBloc] and updates the game accordingly.
|
||||||
|
class GameBlocStatusListener extends Component
|
||||||
|
with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
|
||||||
|
@override
|
||||||
|
bool listenWhen(GameState? previousState, GameState newState) {
|
||||||
|
return previousState?.status != newState.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onNewState(GameState state) {
|
||||||
|
switch (state.status) {
|
||||||
|
case GameStatus.waiting:
|
||||||
|
break;
|
||||||
|
case GameStatus.playing:
|
||||||
|
gameRef.player.play(PinballAudio.backgroundMusic);
|
||||||
|
gameRef.firstChild<CameraController>()?.focusOnGame();
|
||||||
|
gameRef.overlays.remove(PinballGame.playButtonOverlay);
|
||||||
|
break;
|
||||||
|
case GameStatus.gameOver:
|
||||||
|
gameRef.descendants().whereType<Backbox>().first.initialsInput(
|
||||||
|
score: state.displayScore,
|
||||||
|
characterIconPath: gameRef.characterTheme.leaderboardIcon.keyName,
|
||||||
|
);
|
||||||
|
gameRef.firstChild<CameraController>()!.focusOnGameOverBackbox();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,45 +0,0 @@
|
|||||||
import 'package:flame/components.dart';
|
|
||||||
import 'package:flame_bloc/flame_bloc.dart';
|
|
||||||
import 'package:pinball/game/game.dart';
|
|
||||||
import 'package:pinball_flame/pinball_flame.dart';
|
|
||||||
|
|
||||||
/// {@template game_flow_controller}
|
|
||||||
/// A [Component] that controls the game over and game restart logic
|
|
||||||
/// {@endtemplate}
|
|
||||||
class GameFlowController extends ComponentController<PinballGame>
|
|
||||||
with BlocComponent<GameBloc, GameState> {
|
|
||||||
/// {@macro game_flow_controller}
|
|
||||||
GameFlowController(PinballGame component) : super(component);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool listenWhen(GameState? previousState, GameState newState) {
|
|
||||||
return previousState?.isGameOver != newState.isGameOver;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onNewState(GameState state) {
|
|
||||||
if (state.isGameOver) {
|
|
||||||
_initialsInput();
|
|
||||||
} else {
|
|
||||||
start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Puts the game in the initials input state.
|
|
||||||
void _initialsInput() {
|
|
||||||
// TODO(erickzanardo): implement score submission and "navigate" to the
|
|
||||||
// next page
|
|
||||||
component.descendants().whereType<Backbox>().first.initialsInput(
|
|
||||||
score: state?.displayScore ?? 0,
|
|
||||||
characterIconPath: component.characterTheme.leaderboardIcon.keyName,
|
|
||||||
);
|
|
||||||
component.firstChild<CameraController>()!.focusOnGameOverBackbox();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Puts the game in the playing state.
|
|
||||||
void start() {
|
|
||||||
component.audio.backgroundMusic();
|
|
||||||
component.firstChild<CameraController>()?.focusOnGame();
|
|
||||||
component.overlays.remove(PinballGame.playButtonOverlay);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +1,2 @@
|
|||||||
export 'bloc/start_game_bloc.dart';
|
export 'bloc/start_game_bloc.dart';
|
||||||
|
export 'widgets/start_game_listener.dart';
|
||||||
|
@ -0,0 +1,88 @@
|
|||||||
|
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_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,
|
||||||
|
}) : _child = child,
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
final Widget _child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocListener<StartGameBloc, StartGameState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
switch (state.status) {
|
||||||
|
case StartGameStatus.initial:
|
||||||
|
break;
|
||||||
|
case StartGameStatus.selectCharacter:
|
||||||
|
_onSelectCharacter(context);
|
||||||
|
context.read<GameBloc>().add(const GameStarted());
|
||||||
|
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) {
|
||||||
|
_showPinballDialog(
|
||||||
|
context: context,
|
||||||
|
child: HowToPlayDialog(
|
||||||
|
onDismissCallback: () {
|
||||||
|
context.read<StartGameBloc>().add(const HowToPlayFinished());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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: 616 KiB After Width: | Height: | Size: 636 KiB |
Before Width: | Height: | Size: 735 KiB After Width: | Height: | Size: 259 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 |
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,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];
|
||||||
|
}
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |