@ -1,7 +1,33 @@
|
|||||||
{
|
{
|
||||||
|
"firestore": {
|
||||||
|
"rules": "firestore.rules"
|
||||||
|
},
|
||||||
"hosting": {
|
"hosting": {
|
||||||
"public": "build/web",
|
"public": "build/web",
|
||||||
"site": "io-pinball",
|
"site": "io-pinball",
|
||||||
"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,15 @@
|
|||||||
|
// 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_audio/pinball_audio.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.player.play(PinballAudio.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 '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,108 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
||||||
|
import 'package:pinball/game/components/backbox/bloc/backbox_bloc.dart';
|
||||||
|
import 'package:pinball/game/components/backbox/displays/displays.dart';
|
||||||
|
import 'package:pinball/game/pinball_game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
import 'package:pinball_theme/pinball_theme.dart' hide Assets;
|
||||||
|
|
||||||
|
/// {@template backbox}
|
||||||
|
/// The [Backbox] of the pinball machine.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class Backbox extends PositionComponent with HasGameRef<PinballGame>, ZIndex {
|
||||||
|
/// {@macro backbox}
|
||||||
|
Backbox({
|
||||||
|
required LeaderboardRepository leaderboardRepository,
|
||||||
|
}) : _bloc = BackboxBloc(leaderboardRepository: leaderboardRepository);
|
||||||
|
|
||||||
|
/// {@macro backbox}
|
||||||
|
@visibleForTesting
|
||||||
|
Backbox.test({
|
||||||
|
required BackboxBloc bloc,
|
||||||
|
}) : _bloc = bloc;
|
||||||
|
|
||||||
|
late final Component _display;
|
||||||
|
final BackboxBloc _bloc;
|
||||||
|
late StreamSubscription<BackboxState> _subscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
position = Vector2(0, -87);
|
||||||
|
anchor = Anchor.bottomCenter;
|
||||||
|
zIndex = ZIndexes.backbox;
|
||||||
|
|
||||||
|
await add(_BackboxSpriteComponent());
|
||||||
|
await add(_display = Component());
|
||||||
|
|
||||||
|
_subscription = _bloc.stream.listen((state) {
|
||||||
|
_display.children.removeWhere((_) => true);
|
||||||
|
_build(state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onRemove() {
|
||||||
|
super.onRemove();
|
||||||
|
_subscription.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _build(BackboxState state) {
|
||||||
|
if (state is LoadingState) {
|
||||||
|
_display.add(LoadingDisplay());
|
||||||
|
} else if (state is InitialsFormState) {
|
||||||
|
_display.add(
|
||||||
|
InitialsInputDisplay(
|
||||||
|
score: state.score,
|
||||||
|
characterIconPath: state.character.leaderboardIcon.keyName,
|
||||||
|
onSubmit: (initials) {
|
||||||
|
_bloc.add(
|
||||||
|
PlayerInitialsSubmitted(
|
||||||
|
score: state.score,
|
||||||
|
initials: initials,
|
||||||
|
character: state.character,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (state is InitialsSuccessState) {
|
||||||
|
_display.add(InitialsSubmissionSuccessDisplay());
|
||||||
|
} else if (state is InitialsFailureState) {
|
||||||
|
_display.add(InitialsSubmissionFailureDisplay());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Puts [InitialsInputDisplay] on the [Backbox].
|
||||||
|
void requestInitials({
|
||||||
|
required int score,
|
||||||
|
required CharacterTheme character,
|
||||||
|
}) {
|
||||||
|
_bloc.add(
|
||||||
|
PlayerInitialsRequested(
|
||||||
|
score: score,
|
||||||
|
character: character,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,56 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
||||||
|
import 'package:pinball/leaderboard/models/leader_board_entry.dart';
|
||||||
|
import 'package:pinball_theme/pinball_theme.dart';
|
||||||
|
|
||||||
|
part 'backbox_event.dart';
|
||||||
|
part 'backbox_state.dart';
|
||||||
|
|
||||||
|
/// {@template backbox_bloc}
|
||||||
|
/// Bloc which manages the Backbox display.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class BackboxBloc extends Bloc<BackboxEvent, BackboxState> {
|
||||||
|
/// {@macro backbox_bloc}
|
||||||
|
BackboxBloc({
|
||||||
|
required LeaderboardRepository leaderboardRepository,
|
||||||
|
}) : _leaderboardRepository = leaderboardRepository,
|
||||||
|
super(LoadingState()) {
|
||||||
|
on<PlayerInitialsRequested>(_onPlayerInitialsRequested);
|
||||||
|
on<PlayerInitialsSubmitted>(_onPlayerInitialsSubmitted);
|
||||||
|
}
|
||||||
|
|
||||||
|
final LeaderboardRepository _leaderboardRepository;
|
||||||
|
|
||||||
|
void _onPlayerInitialsRequested(
|
||||||
|
PlayerInitialsRequested event,
|
||||||
|
Emitter<BackboxState> emit,
|
||||||
|
) {
|
||||||
|
emit(
|
||||||
|
InitialsFormState(
|
||||||
|
score: event.score,
|
||||||
|
character: event.character,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onPlayerInitialsSubmitted(
|
||||||
|
PlayerInitialsSubmitted event,
|
||||||
|
Emitter<BackboxState> emit,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
emit(LoadingState());
|
||||||
|
await _leaderboardRepository.addLeaderboardEntry(
|
||||||
|
LeaderboardEntryData(
|
||||||
|
playerInitials: event.initials,
|
||||||
|
score: event.score,
|
||||||
|
character: event.character.toType,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
emit(InitialsSuccessState());
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
addError(error, stackTrace);
|
||||||
|
emit(InitialsFailureState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
part of 'backbox_bloc.dart';
|
||||||
|
|
||||||
|
/// {@template backbox_event}
|
||||||
|
/// Base class for backbox events.
|
||||||
|
/// {@endtemplate}
|
||||||
|
abstract class BackboxEvent extends Equatable {
|
||||||
|
/// {@macro backbox_event}
|
||||||
|
const BackboxEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template player_initials_requested}
|
||||||
|
/// Event that triggers the user initials display.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class PlayerInitialsRequested extends BackboxEvent {
|
||||||
|
/// {@macro player_initials_requested}
|
||||||
|
const PlayerInitialsRequested({
|
||||||
|
required this.score,
|
||||||
|
required this.character,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Player's score.
|
||||||
|
final int score;
|
||||||
|
|
||||||
|
/// Player's character.
|
||||||
|
final CharacterTheme character;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [score, character];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template player_initials_submitted}
|
||||||
|
/// Event that submits the user score and initials.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class PlayerInitialsSubmitted extends BackboxEvent {
|
||||||
|
/// {@macro player_initials_submitted}
|
||||||
|
const PlayerInitialsSubmitted({
|
||||||
|
required this.score,
|
||||||
|
required this.initials,
|
||||||
|
required this.character,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Player's score.
|
||||||
|
final int score;
|
||||||
|
|
||||||
|
/// Player's initials.
|
||||||
|
final String initials;
|
||||||
|
|
||||||
|
/// Player's character.
|
||||||
|
final CharacterTheme character;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [score, initials, character];
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
part of 'backbox_bloc.dart';
|
||||||
|
|
||||||
|
/// {@template backbox_state}
|
||||||
|
/// The base state for all [BackboxState].
|
||||||
|
/// {@endtemplate backbox_state}
|
||||||
|
abstract class BackboxState extends Equatable {
|
||||||
|
/// {@macro backbox_state}
|
||||||
|
const BackboxState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loading state for the backbox.
|
||||||
|
class LoadingState extends BackboxState {
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State when the leaderboard was successfully loaded.
|
||||||
|
class LeaderboardSuccessState extends BackboxState {
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State when the leaderboard failed to load.
|
||||||
|
class LeaderboardFailureState extends BackboxState {
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template initials_form_state}
|
||||||
|
/// State when the user is inputting their initials.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class InitialsFormState extends BackboxState {
|
||||||
|
/// {@macro initials_form_state}
|
||||||
|
const InitialsFormState({
|
||||||
|
required this.score,
|
||||||
|
required this.character,
|
||||||
|
}) : super();
|
||||||
|
|
||||||
|
/// Player's score.
|
||||||
|
final int score;
|
||||||
|
|
||||||
|
/// Player's character.
|
||||||
|
final CharacterTheme character;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [score, character];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State when the leaderboard was successfully loaded.
|
||||||
|
class InitialsSuccessState extends BackboxState {
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State when the initials submission failed.
|
||||||
|
class InitialsFailureState extends BackboxState {
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
export 'initials_input_display.dart';
|
||||||
|
export 'initials_submission_failure_display.dart';
|
||||||
|
export 'initials_submission_success_display.dart';
|
||||||
|
export 'loading_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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_ui/pinball_ui.dart';
|
||||||
|
|
||||||
|
final _bodyTextPaint = TextPaint(
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 3,
|
||||||
|
color: PinballColors.white,
|
||||||
|
fontFamily: PinballFonts.pixeloidSans,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// {@template initials_submission_failure_display}
|
||||||
|
/// [Backbox] display for when a failure occurs during initials submission.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class InitialsSubmissionFailureDisplay extends TextComponent
|
||||||
|
with HasGameRef<PinballGame> {
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
position = Vector2(0, -10);
|
||||||
|
anchor = Anchor.center;
|
||||||
|
text = 'Failure!';
|
||||||
|
textRenderer = _bodyTextPaint;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_ui/pinball_ui.dart';
|
||||||
|
|
||||||
|
final _bodyTextPaint = TextPaint(
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 3,
|
||||||
|
color: PinballColors.white,
|
||||||
|
fontFamily: PinballFonts.pixeloidSans,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// {@template initials_submission_success_display}
|
||||||
|
/// [Backbox] display for initials successfully submitted.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class InitialsSubmissionSuccessDisplay extends TextComponent
|
||||||
|
with HasGameRef<PinballGame> {
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
position = Vector2(0, -10);
|
||||||
|
anchor = Anchor.center;
|
||||||
|
text = 'Success!';
|
||||||
|
textRenderer = _bodyTextPaint;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_ui/pinball_ui.dart';
|
||||||
|
|
||||||
|
final _bodyTextPaint = TextPaint(
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 3,
|
||||||
|
color: PinballColors.white,
|
||||||
|
fontFamily: PinballFonts.pixeloidSans,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// {@template loading_display}
|
||||||
|
/// Display used to show the loading animation.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class LoadingDisplay extends TextComponent with HasGameRef<PinballGame> {
|
||||||
|
/// {@template loading_display}
|
||||||
|
LoadingDisplay();
|
||||||
|
|
||||||
|
late final String _label;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
|
||||||
|
position = Vector2(0, -10);
|
||||||
|
anchor = Anchor.center;
|
||||||
|
text = _label = gameRef.l10n.loading;
|
||||||
|
textRenderer = _bodyTextPaint;
|
||||||
|
|
||||||
|
await add(
|
||||||
|
TimerComponent(
|
||||||
|
period: 1,
|
||||||
|
repeat: true,
|
||||||
|
onTick: () {
|
||||||
|
final index = text.indexOf('.');
|
||||||
|
if (index != -1 && text.substring(index).length == 3) {
|
||||||
|
text = _label;
|
||||||
|
} else {
|
||||||
|
text = '$text.';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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.requestInitials(
|
||||||
|
score: state.displayScore,
|
||||||
|
character: gameRef.characterTheme,
|
||||||
|
);
|
||||||
|
gameRef.firstChild<CameraController>()!.focusOnGameOverBackbox();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,47 +0,0 @@
|
|||||||
import 'package:flame/components.dart';
|
|
||||||
import 'package:flame_bloc/flame_bloc.dart';
|
|
||||||
import 'package:pinball/game/game.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart';
|
|
||||||
import 'package:pinball_flame/pinball_flame.dart';
|
|
||||||
|
|
||||||
/// {@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) {
|
|
||||||
gameOver();
|
|
||||||
} else {
|
|
||||||
start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Puts the game on a game over state
|
|
||||||
void gameOver() {
|
|
||||||
// TODO(erickzanardo): implement score submission and "navigate" to the
|
|
||||||
// next page
|
|
||||||
component.firstChild<Backboard>()?.gameOverMode(
|
|
||||||
score: state?.score ?? 0,
|
|
||||||
characterIconPath: component.characterTheme.leaderboardIcon.keyName,
|
|
||||||
);
|
|
||||||
component.firstChild<CameraController>()?.focusOnBackboard();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Puts the game on a playing state
|
|
||||||
void start() {
|
|
||||||
component.audio.backgroundMusic();
|
|
||||||
component.firstChild<Backboard>()?.waitingMode();
|
|
||||||
component.firstChild<CameraController>()?.focusOnGame();
|
|
||||||
component.overlays.remove(PinballGame.playButtonOverlay);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 '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: 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 |
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 |
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_gravitating_behavior.dart';
|
||||||
export 'ball_scaling_behavior.dart';
|
export 'ball_scaling_behavior.dart';
|
||||||
|
export 'ball_turbo_charging_behavior.dart';
|
||||||
|