@ -0,0 +1,25 @@
|
|||||||
|
name: deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-dev:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Deploy Development
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
- run: flutter packages get
|
||||||
|
- run: flutter build web --target lib/main_development.dart --web-renderer canvaskit --release
|
||||||
|
- uses: FirebaseExtended/action-hosting-deploy@v0
|
||||||
|
with:
|
||||||
|
repoToken: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_PINBALL_DEV }}"
|
||||||
|
channelId: live
|
||||||
|
projectId: pinball-dev
|
||||||
|
target: ashehwkdkdjruejdnensjsjdne
|
@ -0,0 +1,23 @@
|
|||||||
|
name: pinball_ui
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "packages/pinball_ui/**"
|
||||||
|
- ".github/workflows/pinball_ui.yaml"
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "packages/pinball_ui/**"
|
||||||
|
- ".github/workflows/pinball_ui.yaml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
|
||||||
|
with:
|
||||||
|
working_directory: packages/pinball_ui
|
||||||
|
coverage_excludes: "lib/gen/*.dart"
|
@ -0,0 +1,23 @@
|
|||||||
|
name: platform_helper
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "packages/platform_helper/**"
|
||||||
|
- ".github/workflows/platform_helper.yaml"
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "packages/platform_helper/**"
|
||||||
|
- ".github/workflows/platform_helper.yaml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
|
||||||
|
with:
|
||||||
|
working_directory: packages/platform_helper
|
||||||
|
coverage_excludes: "lib/gen/*.dart"
|
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.6 KiB |
@ -1,86 +0,0 @@
|
|||||||
import 'package:flame/components.dart';
|
|
||||||
import 'package:pinball/game/game.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart';
|
|
||||||
|
|
||||||
/// {@template board}
|
|
||||||
/// The main flat surface of the [PinballGame].
|
|
||||||
/// {endtemplate}
|
|
||||||
class Board extends Component {
|
|
||||||
/// {@macro board}
|
|
||||||
// TODO(alestiago): Make Board a Blueprint and sort out priorities.
|
|
||||||
Board() : super(priority: 1);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
// TODO(allisonryan0002): add bottom group and flutter forest to pinball
|
|
||||||
// game directly. Then remove board.
|
|
||||||
final bottomGroup = _BottomGroup();
|
|
||||||
|
|
||||||
final flutterForest = FlutterForest();
|
|
||||||
|
|
||||||
await addAll([
|
|
||||||
bottomGroup,
|
|
||||||
flutterForest,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template bottom_group}
|
|
||||||
/// Grouping of the board's bottom [Component]s.
|
|
||||||
///
|
|
||||||
/// The [_BottomGroup] consists of[Flipper]s, [Baseboard]s and [Kicker]s.
|
|
||||||
/// {@endtemplate}
|
|
||||||
// TODO(alestiago): Consider renaming once entire Board is defined.
|
|
||||||
class _BottomGroup extends Component {
|
|
||||||
/// {@macro bottom_group}
|
|
||||||
_BottomGroup() : super(priority: RenderPriority.bottomGroup);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
final rightSide = _BottomGroupSide(
|
|
||||||
side: BoardSide.right,
|
|
||||||
);
|
|
||||||
final leftSide = _BottomGroupSide(
|
|
||||||
side: BoardSide.left,
|
|
||||||
);
|
|
||||||
|
|
||||||
await addAll([rightSide, leftSide]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template bottom_group_side}
|
|
||||||
/// Group with one side of [_BottomGroup]'s symmetric [Component]s.
|
|
||||||
///
|
|
||||||
/// For example, [Flipper]s are symmetric components.
|
|
||||||
/// {@endtemplate}
|
|
||||||
class _BottomGroupSide extends Component {
|
|
||||||
/// {@macro bottom_group_side}
|
|
||||||
_BottomGroupSide({
|
|
||||||
required BoardSide side,
|
|
||||||
}) : _side = side;
|
|
||||||
|
|
||||||
final BoardSide _side;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
final direction = _side.direction;
|
|
||||||
final centerXAdjustment = _side.isLeft ? 0 : -6.5;
|
|
||||||
|
|
||||||
final flipper = ControlledFlipper(
|
|
||||||
side: _side,
|
|
||||||
)..initialPosition = Vector2((11.8 * direction) + centerXAdjustment, 43.6);
|
|
||||||
final baseboard = Baseboard(side: _side)
|
|
||||||
..initialPosition = Vector2(
|
|
||||||
(25.58 * direction) + centerXAdjustment,
|
|
||||||
28.69,
|
|
||||||
);
|
|
||||||
final kicker = Kicker(
|
|
||||||
side: _side,
|
|
||||||
)..initialPosition = Vector2(
|
|
||||||
(22.4 * direction) + centerXAdjustment,
|
|
||||||
25,
|
|
||||||
);
|
|
||||||
|
|
||||||
await addAll([flipper, baseboard, kicker]);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,61 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
/// {@template bottom_group}
|
||||||
|
/// Grouping of the board's symmetrical bottom [Component]s.
|
||||||
|
///
|
||||||
|
/// The [BottomGroup] consists of [Flipper]s, [Baseboard]s and [Kicker]s.
|
||||||
|
/// {@endtemplate}
|
||||||
|
// TODO(allisonryan0002): Consider renaming.
|
||||||
|
class BottomGroup extends Component {
|
||||||
|
/// {@macro bottom_group}
|
||||||
|
BottomGroup()
|
||||||
|
: super(
|
||||||
|
children: [
|
||||||
|
_BottomGroupSide(side: BoardSide.right),
|
||||||
|
_BottomGroupSide(side: BoardSide.left),
|
||||||
|
],
|
||||||
|
priority: RenderPriority.bottomGroup,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template bottom_group_side}
|
||||||
|
/// Group with one side of [BottomGroup]'s symmetric [Component]s.
|
||||||
|
///
|
||||||
|
/// For example, [Flipper]s are symmetric components.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class _BottomGroupSide extends Component {
|
||||||
|
/// {@macro bottom_group_side}
|
||||||
|
_BottomGroupSide({
|
||||||
|
required BoardSide side,
|
||||||
|
}) : _side = side;
|
||||||
|
|
||||||
|
final BoardSide _side;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
final direction = _side.direction;
|
||||||
|
final centerXAdjustment = _side.isLeft ? 0 : -6.5;
|
||||||
|
|
||||||
|
final flipper = ControlledFlipper(
|
||||||
|
side: _side,
|
||||||
|
)..initialPosition = Vector2((11.8 * direction) + centerXAdjustment, 43.6);
|
||||||
|
final baseboard = Baseboard(side: _side)
|
||||||
|
..initialPosition = Vector2(
|
||||||
|
(25.58 * direction) + centerXAdjustment,
|
||||||
|
28.69,
|
||||||
|
);
|
||||||
|
final kicker = Kicker(
|
||||||
|
side: _side,
|
||||||
|
children: [
|
||||||
|
ScoringBehavior(points: 5000),
|
||||||
|
],
|
||||||
|
)..initialPosition = Vector2(
|
||||||
|
(22.4 * direction) + centerXAdjustment,
|
||||||
|
25,
|
||||||
|
);
|
||||||
|
|
||||||
|
await addAll([flipper, baseboard, kicker]);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
|
/// {@template dino_desert}
|
||||||
|
/// Area located next to the [Launcher] containing the [ChromeDino] and
|
||||||
|
/// [DinoWalls].
|
||||||
|
/// {@endtemplate}
|
||||||
|
// TODO(allisonryan0002): use a controller to initiate dino bonus when dino is
|
||||||
|
// fully implemented.
|
||||||
|
class DinoDesert extends Blueprint {
|
||||||
|
/// {@macro dino_desert}
|
||||||
|
DinoDesert()
|
||||||
|
: super(
|
||||||
|
components: [
|
||||||
|
ChromeDino()..initialPosition = Vector2(12.3, -6.9),
|
||||||
|
],
|
||||||
|
blueprints: [
|
||||||
|
DinoWalls(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
import 'package:flame/extensions.dart';
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
/// {@template drain}
|
||||||
|
/// Area located at the bottom of the board to detect when a [Ball] is lost.
|
||||||
|
/// {@endtemplate}
|
||||||
|
// TODO(allisonryan0002): move to components package when possible.
|
||||||
|
class Drain extends BodyComponent with ContactCallbacks {
|
||||||
|
/// {@macro drain}
|
||||||
|
Drain() : super(renderBody: false);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Body createBody() {
|
||||||
|
final shape = EdgeShape()
|
||||||
|
..set(
|
||||||
|
BoardDimensions.bounds.bottomLeft.toVector2(),
|
||||||
|
BoardDimensions.bounds.bottomRight.toVector2(),
|
||||||
|
);
|
||||||
|
final fixtureDef = FixtureDef(shape, isSensor: true);
|
||||||
|
final bodyDef = BodyDef(userData: this);
|
||||||
|
|
||||||
|
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(allisonryan0002): move this to ball.dart when BallLost is removed.
|
||||||
|
@override
|
||||||
|
void beginContact(Object other, Contact contact) {
|
||||||
|
super.beginContact(other, contact);
|
||||||
|
if (other is! ControlledBall) return;
|
||||||
|
other.controller.lost();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export 'multipliers_behavior.dart';
|
@ -0,0 +1,25 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_bloc/flame_bloc.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
|
/// Toggle each [Multiplier] when GameState.multiplier changes.
|
||||||
|
class MultipliersBehavior extends Component
|
||||||
|
with
|
||||||
|
HasGameRef<PinballGame>,
|
||||||
|
ParentIsA<Multipliers>,
|
||||||
|
BlocComponent<GameBloc, GameState> {
|
||||||
|
@override
|
||||||
|
bool listenWhen(GameState? previousState, GameState newState) {
|
||||||
|
return previousState?.multiplier != newState.multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onNewState(GameState state) {
|
||||||
|
final multipliers = parent.children.whereType<Multiplier>();
|
||||||
|
for (final multiplier in multipliers) {
|
||||||
|
multiplier.bloc.next(state.multiplier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pinball/game/components/multipliers/behaviors/behaviors.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
/// {@template multipliers}
|
||||||
|
/// A group for the multipliers on the board.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class Multipliers extends Component {
|
||||||
|
/// {@macro multipliers}
|
||||||
|
Multipliers()
|
||||||
|
: super(
|
||||||
|
children: [
|
||||||
|
Multiplier.x2(
|
||||||
|
position: Vector2(-19.5, -2),
|
||||||
|
angle: -15 * math.pi / 180,
|
||||||
|
),
|
||||||
|
Multiplier.x3(
|
||||||
|
position: Vector2(13, -9.4),
|
||||||
|
angle: 15 * math.pi / 180,
|
||||||
|
),
|
||||||
|
Multiplier.x4(
|
||||||
|
position: Vector2(0, -21.2),
|
||||||
|
angle: 0,
|
||||||
|
),
|
||||||
|
Multiplier.x5(
|
||||||
|
position: Vector2(-8.5, -28),
|
||||||
|
angle: -3 * math.pi / 180,
|
||||||
|
),
|
||||||
|
Multiplier.x6(
|
||||||
|
position: Vector2(10, -30.7),
|
||||||
|
angle: 8 * math.pi / 180,
|
||||||
|
),
|
||||||
|
MultipliersBehavior(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Creates [Multipliers] without any children.
|
||||||
|
///
|
||||||
|
/// This can be used for testing [Multipliers]'s behaviors in isolation.
|
||||||
|
@visibleForTesting
|
||||||
|
Multipliers.test();
|
||||||
|
}
|
@ -1,60 +0,0 @@
|
|||||||
// ignore_for_file: avoid_renaming_method_parameters
|
|
||||||
|
|
||||||
import 'package:flame/extensions.dart';
|
|
||||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
|
||||||
import 'package:pinball/game/game.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart' hide Assets;
|
|
||||||
|
|
||||||
/// {@template wall}
|
|
||||||
/// A continuous generic and [BodyType.static] barrier that divides a game area.
|
|
||||||
/// {@endtemplate}
|
|
||||||
// TODO(alestiago): Remove [Wall] for [Pathway.straight].
|
|
||||||
class Wall extends BodyComponent {
|
|
||||||
/// {@macro wall}
|
|
||||||
Wall({
|
|
||||||
required this.start,
|
|
||||||
required this.end,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The [start] of the [Wall].
|
|
||||||
final Vector2 start;
|
|
||||||
|
|
||||||
/// The [end] of the [Wall].
|
|
||||||
final Vector2 end;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Body createBody() {
|
|
||||||
final shape = EdgeShape()..set(start, end);
|
|
||||||
|
|
||||||
final fixtureDef = FixtureDef(shape)
|
|
||||||
..restitution = 0.1
|
|
||||||
..friction = 0;
|
|
||||||
|
|
||||||
final bodyDef = BodyDef()
|
|
||||||
..userData = this
|
|
||||||
..position = Vector2.zero()
|
|
||||||
..type = BodyType.static;
|
|
||||||
|
|
||||||
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template bottom_wall}
|
|
||||||
/// [Wall] located at the bottom of the board.
|
|
||||||
///
|
|
||||||
/// {@endtemplate}
|
|
||||||
class BottomWall extends Wall with ContactCallbacks {
|
|
||||||
/// {@macro bottom_wall}
|
|
||||||
BottomWall()
|
|
||||||
: super(
|
|
||||||
start: BoardDimensions.bounds.bottomLeft.toVector2(),
|
|
||||||
end: BoardDimensions.bounds.bottomRight.toVector2(),
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void beginContact(Object other, Contact contact) {
|
|
||||||
super.beginContact(other, contact);
|
|
||||||
if (other is! ControlledBall) return;
|
|
||||||
other.controller.lost();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
|
||||||
import 'package:pinball/leaderboard/leaderboard.dart';
|
|
||||||
|
|
||||||
part 'leaderboard_event.dart';
|
|
||||||
part 'leaderboard_state.dart';
|
|
||||||
|
|
||||||
/// {@template leaderboard_bloc}
|
|
||||||
/// Manages leaderboard events.
|
|
||||||
///
|
|
||||||
/// Uses a [LeaderboardRepository] to request and update players participations.
|
|
||||||
/// {@endtemplate}
|
|
||||||
class LeaderboardBloc extends Bloc<LeaderboardEvent, LeaderboardState> {
|
|
||||||
/// {@macro leaderboard_bloc}
|
|
||||||
LeaderboardBloc(this._leaderboardRepository)
|
|
||||||
: super(const LeaderboardState.initial()) {
|
|
||||||
on<Top10Fetched>(_onTop10Fetched);
|
|
||||||
on<LeaderboardEntryAdded>(_onLeaderboardEntryAdded);
|
|
||||||
}
|
|
||||||
|
|
||||||
final LeaderboardRepository _leaderboardRepository;
|
|
||||||
|
|
||||||
Future<void> _onTop10Fetched(
|
|
||||||
Top10Fetched event,
|
|
||||||
Emitter<LeaderboardState> emit,
|
|
||||||
) async {
|
|
||||||
emit(state.copyWith(status: LeaderboardStatus.loading));
|
|
||||||
try {
|
|
||||||
final top10Leaderboard =
|
|
||||||
await _leaderboardRepository.fetchTop10Leaderboard();
|
|
||||||
|
|
||||||
final leaderboardEntries = <LeaderboardEntry>[];
|
|
||||||
top10Leaderboard.asMap().forEach(
|
|
||||||
(index, value) => leaderboardEntries.add(value.toEntry(index + 1)),
|
|
||||||
);
|
|
||||||
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: LeaderboardStatus.success,
|
|
||||||
leaderboard: leaderboardEntries,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
emit(state.copyWith(status: LeaderboardStatus.error));
|
|
||||||
addError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onLeaderboardEntryAdded(
|
|
||||||
LeaderboardEntryAdded event,
|
|
||||||
Emitter<LeaderboardState> emit,
|
|
||||||
) async {
|
|
||||||
emit(state.copyWith(status: LeaderboardStatus.loading));
|
|
||||||
try {
|
|
||||||
final ranking =
|
|
||||||
await _leaderboardRepository.addLeaderboardEntry(event.entry);
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: LeaderboardStatus.success,
|
|
||||||
ranking: ranking,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
emit(state.copyWith(status: LeaderboardStatus.error));
|
|
||||||
addError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
part of 'leaderboard_bloc.dart';
|
|
||||||
|
|
||||||
/// {@template leaderboard_event}
|
|
||||||
/// Represents the events available for [LeaderboardBloc].
|
|
||||||
/// {endtemplate}
|
|
||||||
abstract class LeaderboardEvent extends Equatable {
|
|
||||||
/// {@macro leaderboard_event}
|
|
||||||
const LeaderboardEvent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template top_10_fetched}
|
|
||||||
/// Request the top 10 [LeaderboardEntryData]s.
|
|
||||||
/// {endtemplate}
|
|
||||||
class Top10Fetched extends LeaderboardEvent {
|
|
||||||
/// {@macro top_10_fetched}
|
|
||||||
const Top10Fetched();
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template leaderboard_entry_added}
|
|
||||||
/// Writes a new [LeaderboardEntryData].
|
|
||||||
///
|
|
||||||
/// Should be added when a player finishes a game.
|
|
||||||
/// {endtemplate}
|
|
||||||
class LeaderboardEntryAdded extends LeaderboardEvent {
|
|
||||||
/// {@macro leaderboard_entry_added}
|
|
||||||
const LeaderboardEntryAdded({required this.entry});
|
|
||||||
|
|
||||||
/// [LeaderboardEntryData] to be written to the remote storage.
|
|
||||||
final LeaderboardEntryData entry;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [entry];
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
// ignore_for_file: public_member_api_docs
|
|
||||||
|
|
||||||
part of 'leaderboard_bloc.dart';
|
|
||||||
|
|
||||||
/// Defines the request status.
|
|
||||||
enum LeaderboardStatus {
|
|
||||||
/// Request is being loaded.
|
|
||||||
loading,
|
|
||||||
|
|
||||||
/// Request was processed successfully and received a valid response.
|
|
||||||
success,
|
|
||||||
|
|
||||||
/// Request was processed unsuccessfully and received an error.
|
|
||||||
error,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template leaderboard_state}
|
|
||||||
/// Represents the state of the leaderboard.
|
|
||||||
/// {@endtemplate}
|
|
||||||
class LeaderboardState extends Equatable {
|
|
||||||
/// {@macro leaderboard_state}
|
|
||||||
const LeaderboardState({
|
|
||||||
required this.status,
|
|
||||||
required this.ranking,
|
|
||||||
required this.leaderboard,
|
|
||||||
});
|
|
||||||
|
|
||||||
const LeaderboardState.initial()
|
|
||||||
: status = LeaderboardStatus.loading,
|
|
||||||
ranking = const LeaderboardRanking(
|
|
||||||
ranking: 0,
|
|
||||||
outOf: 0,
|
|
||||||
),
|
|
||||||
leaderboard = const [];
|
|
||||||
|
|
||||||
/// The current [LeaderboardStatus] of the state.
|
|
||||||
final LeaderboardStatus status;
|
|
||||||
|
|
||||||
/// Rank of the current player.
|
|
||||||
final LeaderboardRanking ranking;
|
|
||||||
|
|
||||||
/// List of top-ranked players.
|
|
||||||
final List<LeaderboardEntry> leaderboard;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object> get props => [status, ranking, leaderboard];
|
|
||||||
|
|
||||||
LeaderboardState copyWith({
|
|
||||||
LeaderboardStatus? status,
|
|
||||||
LeaderboardRanking? ranking,
|
|
||||||
List<LeaderboardEntry>? leaderboard,
|
|
||||||
}) {
|
|
||||||
return LeaderboardState(
|
|
||||||
status: status ?? this.status,
|
|
||||||
ranking: ranking ?? this.ranking,
|
|
||||||
leaderboard: leaderboard ?? this.leaderboard,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
export 'bloc/leaderboard_bloc.dart';
|
|
||||||
export 'models/leader_board_entry.dart';
|
|
||||||
export 'view/leaderboard_page.dart';
|
|
@ -1,306 +0,0 @@
|
|||||||
// ignore_for_file: public_member_api_docs
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
|
||||||
import 'package:pinball/l10n/l10n.dart';
|
|
||||||
import 'package:pinball/leaderboard/leaderboard.dart';
|
|
||||||
import 'package:pinball/select_character/select_character.dart';
|
|
||||||
import 'package:pinball_theme/pinball_theme.dart';
|
|
||||||
|
|
||||||
class LeaderboardPage extends StatelessWidget {
|
|
||||||
const LeaderboardPage({Key? key, required this.theme}) : super(key: key);
|
|
||||||
|
|
||||||
final CharacterTheme theme;
|
|
||||||
|
|
||||||
static Route route({required CharacterTheme theme}) {
|
|
||||||
return MaterialPageRoute<void>(
|
|
||||||
builder: (_) => LeaderboardPage(theme: theme),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocProvider(
|
|
||||||
create: (context) => LeaderboardBloc(
|
|
||||||
context.read<LeaderboardRepository>(),
|
|
||||||
)..add(const Top10Fetched()),
|
|
||||||
child: LeaderboardView(theme: theme),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LeaderboardView extends StatelessWidget {
|
|
||||||
const LeaderboardView({Key? key, required this.theme}) : super(key: key);
|
|
||||||
|
|
||||||
final CharacterTheme theme;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final l10n = context.l10n;
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
body: Center(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 80),
|
|
||||||
Text(
|
|
||||||
l10n.leaderboard,
|
|
||||||
style: Theme.of(context).textTheme.headline3,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 80),
|
|
||||||
BlocBuilder<LeaderboardBloc, LeaderboardState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
switch (state.status) {
|
|
||||||
case LeaderboardStatus.loading:
|
|
||||||
return _LeaderboardLoading(theme: theme);
|
|
||||||
case LeaderboardStatus.success:
|
|
||||||
return _LeaderboardRanking(
|
|
||||||
ranking: state.leaderboard,
|
|
||||||
theme: theme,
|
|
||||||
);
|
|
||||||
case LeaderboardStatus.error:
|
|
||||||
return _LeaderboardError(theme: theme);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).push<void>(
|
|
||||||
CharacterSelectionDialog.route(),
|
|
||||||
),
|
|
||||||
child: Text(l10n.retry),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LeaderboardLoading extends StatelessWidget {
|
|
||||||
const _LeaderboardLoading({Key? key, required this.theme}) : super(key: key);
|
|
||||||
|
|
||||||
final CharacterTheme theme;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LeaderboardError extends StatelessWidget {
|
|
||||||
const _LeaderboardError({Key? key, required this.theme}) : super(key: key);
|
|
||||||
|
|
||||||
final CharacterTheme theme;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
child: Text(
|
|
||||||
'There was en error loading data!',
|
|
||||||
style:
|
|
||||||
Theme.of(context).textTheme.headline6?.copyWith(color: Colors.red),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LeaderboardRanking extends StatelessWidget {
|
|
||||||
const _LeaderboardRanking({
|
|
||||||
Key? key,
|
|
||||||
required this.ranking,
|
|
||||||
required this.theme,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final List<LeaderboardEntry> ranking;
|
|
||||||
final CharacterTheme theme;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
_LeaderboardHeaders(theme: theme),
|
|
||||||
_LeaderboardList(
|
|
||||||
ranking: ranking,
|
|
||||||
theme: theme,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LeaderboardHeaders extends StatelessWidget {
|
|
||||||
const _LeaderboardHeaders({Key? key, required this.theme}) : super(key: key);
|
|
||||||
|
|
||||||
final CharacterTheme theme;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final l10n = context.l10n;
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
_LeaderboardHeaderItem(title: l10n.rank, theme: theme),
|
|
||||||
_LeaderboardHeaderItem(title: l10n.character, theme: theme),
|
|
||||||
_LeaderboardHeaderItem(title: l10n.username, theme: theme),
|
|
||||||
_LeaderboardHeaderItem(title: l10n.score, theme: theme),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LeaderboardHeaderItem extends StatelessWidget {
|
|
||||||
const _LeaderboardHeaderItem({
|
|
||||||
Key? key,
|
|
||||||
required this.title,
|
|
||||||
required this.theme,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final CharacterTheme theme;
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Expanded(
|
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.ballColor,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: Theme.of(context).textTheme.headline5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LeaderboardList extends StatelessWidget {
|
|
||||||
const _LeaderboardList({
|
|
||||||
Key? key,
|
|
||||||
required this.ranking,
|
|
||||||
required this.theme,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final List<LeaderboardEntry> ranking;
|
|
||||||
final CharacterTheme theme;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemBuilder: (_, index) => _LeaderBoardCompetitor(
|
|
||||||
entry: ranking[index],
|
|
||||||
theme: theme,
|
|
||||||
),
|
|
||||||
itemCount: ranking.length,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LeaderBoardCompetitor extends StatelessWidget {
|
|
||||||
const _LeaderBoardCompetitor({
|
|
||||||
Key? key,
|
|
||||||
required this.entry,
|
|
||||||
required this.theme,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final CharacterTheme theme;
|
|
||||||
|
|
||||||
final LeaderboardEntry entry;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
_LeaderboardCompetitorField(
|
|
||||||
text: entry.rank,
|
|
||||||
theme: theme,
|
|
||||||
),
|
|
||||||
_LeaderboardCompetitorCharacter(
|
|
||||||
characterAsset: entry.character,
|
|
||||||
theme: theme,
|
|
||||||
),
|
|
||||||
_LeaderboardCompetitorField(
|
|
||||||
text: entry.playerInitials,
|
|
||||||
theme: theme,
|
|
||||||
),
|
|
||||||
_LeaderboardCompetitorField(
|
|
||||||
text: entry.score.toString(),
|
|
||||||
theme: theme,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LeaderboardCompetitorField extends StatelessWidget {
|
|
||||||
const _LeaderboardCompetitorField({
|
|
||||||
Key? key,
|
|
||||||
required this.text,
|
|
||||||
required this.theme,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final CharacterTheme theme;
|
|
||||||
final String text;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Expanded(
|
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: theme.ballColor,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Text(text),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LeaderboardCompetitorCharacter extends StatelessWidget {
|
|
||||||
const _LeaderboardCompetitorCharacter({
|
|
||||||
Key? key,
|
|
||||||
required this.characterAsset,
|
|
||||||
required this.theme,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final CharacterTheme theme;
|
|
||||||
final AssetGenImage characterAsset;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Expanded(
|
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: theme.ballColor,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: SizedBox(
|
|
||||||
height: 30,
|
|
||||||
child: characterAsset.image(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,69 @@
|
|||||||
|
/// {@template leaderboard_exception}
|
||||||
|
/// Base exception for leaderboard repository failures.
|
||||||
|
/// {@endtemplate}
|
||||||
|
abstract class LeaderboardException implements Exception {
|
||||||
|
/// {@macro leaderboard_exception}
|
||||||
|
const LeaderboardException(this.error, this.stackTrace);
|
||||||
|
|
||||||
|
/// The error that was caught.
|
||||||
|
final Object error;
|
||||||
|
|
||||||
|
/// The Stacktrace associated with the [error].
|
||||||
|
final StackTrace stackTrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template leaderboard_deserialization_exception}
|
||||||
|
/// Exception thrown when leaderboard data cannot be deserialized in the
|
||||||
|
/// expected way.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class LeaderboardDeserializationException extends LeaderboardException {
|
||||||
|
/// {@macro leaderboard_deserialization_exception}
|
||||||
|
const LeaderboardDeserializationException(Object error, StackTrace stackTrace)
|
||||||
|
: super(error, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template fetch_top_10_leaderboard_exception}
|
||||||
|
/// Exception thrown when failure occurs while fetching top 10 leaderboard.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class FetchTop10LeaderboardException extends LeaderboardException {
|
||||||
|
/// {@macro fetch_top_10_leaderboard_exception}
|
||||||
|
const FetchTop10LeaderboardException(Object error, StackTrace stackTrace)
|
||||||
|
: super(error, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template fetch_leaderboard_exception}
|
||||||
|
/// Exception thrown when failure occurs while fetching the leaderboard.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class FetchLeaderboardException extends LeaderboardException {
|
||||||
|
/// {@macro fetch_top_10_leaderboard_exception}
|
||||||
|
const FetchLeaderboardException(Object error, StackTrace stackTrace)
|
||||||
|
: super(error, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template delete_leaderboard_exception}
|
||||||
|
/// Exception thrown when failure occurs while deleting the leaderboard under
|
||||||
|
/// the tenth position.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class DeleteLeaderboardException extends LeaderboardException {
|
||||||
|
/// {@macro fetch_top_10_leaderboard_exception}
|
||||||
|
const DeleteLeaderboardException(Object error, StackTrace stackTrace)
|
||||||
|
: super(error, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template add_leaderboard_entry_exception}
|
||||||
|
/// Exception thrown when failure occurs while adding entry to leaderboard.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class AddLeaderboardEntryException extends LeaderboardException {
|
||||||
|
/// {@macro add_leaderboard_entry_exception}
|
||||||
|
const AddLeaderboardEntryException(Object error, StackTrace stackTrace)
|
||||||
|
: super(error, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template fetch_prohibited_initials_exception}
|
||||||
|
/// Exception thrown when failure occurs while fetching prohibited initials.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class FetchProhibitedInitialsException extends LeaderboardException {
|
||||||
|
/// {@macro fetch_prohibited_initials_exception}
|
||||||
|
const FetchProhibitedInitialsException(Object error, StackTrace stackTrace)
|
||||||
|
: super(error, stackTrace);
|
||||||
|
}
|
@ -1,20 +0,0 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
|
||||||
|
|
||||||
/// {@template leaderboard_ranking}
|
|
||||||
/// Contains [ranking] for a single [LeaderboardEntryData] and the number of
|
|
||||||
/// players the [ranking] is [outOf].
|
|
||||||
/// {@endtemplate}
|
|
||||||
class LeaderboardRanking extends Equatable {
|
|
||||||
/// {@macro leaderboard_ranking}
|
|
||||||
const LeaderboardRanking({required this.ranking, required this.outOf});
|
|
||||||
|
|
||||||
/// Place ranking by score for a [LeaderboardEntryData].
|
|
||||||
final int ranking;
|
|
||||||
|
|
||||||
/// Number of [LeaderboardEntryData]s at the time of score entry.
|
|
||||||
final int outOf;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object> get props => [ranking, outOf];
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('LeaderboardRanking', () {
|
|
||||||
test('can be instantiated', () {
|
|
||||||
const leaderboardRanking = LeaderboardRanking(ranking: 1, outOf: 1);
|
|
||||||
|
|
||||||
expect(leaderboardRanking, isNotNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('supports value equality.', () {
|
|
||||||
const leaderboardRanking = LeaderboardRanking(ranking: 1, outOf: 1);
|
|
||||||
const leaderboardRanking2 = LeaderboardRanking(ranking: 1, outOf: 1);
|
|
||||||
|
|
||||||
expect(leaderboardRanking, equals(leaderboardRanking2));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 616 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 735 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 231 KiB |
Before Width: | Height: | Size: 38 KiB |
@ -0,0 +1,209 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:pinball_components/gen/assets.gen.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart' hide Assets;
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
|
class AndroidSpaceship extends Blueprint {
|
||||||
|
AndroidSpaceship({required Vector2 position})
|
||||||
|
: super(
|
||||||
|
components: [
|
||||||
|
_SpaceshipSaucer()..initialPosition = position,
|
||||||
|
_SpaceshipSaucerSpriteAnimationComponent()..position = position,
|
||||||
|
_LightBeamSpriteComponent()..position = position + Vector2(2.5, 5),
|
||||||
|
_AndroidHead()..initialPosition = position + Vector2(0.5, 0.25),
|
||||||
|
_SpaceshipHole(
|
||||||
|
outsideLayer: Layer.spaceshipExitRail,
|
||||||
|
outsidePriority: RenderPriority.ballOnSpaceshipRail,
|
||||||
|
)..initialPosition = position - Vector2(5.3, -5.4),
|
||||||
|
_SpaceshipHole(
|
||||||
|
outsideLayer: Layer.board,
|
||||||
|
outsidePriority: RenderPriority.ballOnBoard,
|
||||||
|
)..initialPosition = position - Vector2(-7.5, -1.1),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
|
||||||
|
_SpaceshipSaucer() : super(renderBody: false) {
|
||||||
|
layer = Layer.spaceship;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Body createBody() {
|
||||||
|
final shape = _SpaceshipSaucerShape();
|
||||||
|
final bodyDef = BodyDef(
|
||||||
|
position: initialPosition,
|
||||||
|
userData: this,
|
||||||
|
angle: -1.7,
|
||||||
|
);
|
||||||
|
|
||||||
|
return world.createBody(bodyDef)..createFixtureFromShape(shape);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpaceshipSaucerShape extends ChainShape {
|
||||||
|
_SpaceshipSaucerShape() {
|
||||||
|
const minorRadius = 9.75;
|
||||||
|
const majorRadius = 11.9;
|
||||||
|
|
||||||
|
createChain(
|
||||||
|
[
|
||||||
|
for (var angle = 0.2618; angle <= 6.0214; angle += math.pi / 180)
|
||||||
|
Vector2(
|
||||||
|
minorRadius * math.cos(angle),
|
||||||
|
majorRadius * math.sin(angle),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpaceshipSaucerSpriteAnimationComponent extends SpriteAnimationComponent
|
||||||
|
with HasGameRef {
|
||||||
|
_SpaceshipSaucerSpriteAnimationComponent()
|
||||||
|
: super(
|
||||||
|
anchor: Anchor.center,
|
||||||
|
priority: RenderPriority.spaceshipSaucer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
|
||||||
|
final spriteSheet = gameRef.images.fromCache(
|
||||||
|
Assets.images.android.spaceship.saucer.keyName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const amountPerRow = 5;
|
||||||
|
const amountPerColumn = 3;
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(allisonryan0002): add pulsing behavior.
|
||||||
|
class _LightBeamSpriteComponent extends SpriteComponent with HasGameRef {
|
||||||
|
_LightBeamSpriteComponent()
|
||||||
|
: super(
|
||||||
|
anchor: Anchor.center,
|
||||||
|
priority: RenderPriority.spaceshipLightBeam,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
final sprite = Sprite(
|
||||||
|
gameRef.images.fromCache(
|
||||||
|
Assets.images.android.spaceship.lightBeam.keyName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
this.sprite = sprite;
|
||||||
|
size = sprite.originalSize / 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AndroidHead extends BodyComponent with InitialPosition, Layered {
|
||||||
|
_AndroidHead()
|
||||||
|
: super(
|
||||||
|
priority: RenderPriority.androidHead,
|
||||||
|
children: [_AndroidHeadSpriteAnimationComponent()],
|
||||||
|
renderBody: false,
|
||||||
|
) {
|
||||||
|
layer = Layer.spaceship;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Body createBody() {
|
||||||
|
final shape = EllipseShape(
|
||||||
|
center: Vector2.zero(),
|
||||||
|
majorRadius: 3.1,
|
||||||
|
minorRadius: 2,
|
||||||
|
)..rotate(1.4);
|
||||||
|
// TODO(allisonryan0002): use bumping behavior.
|
||||||
|
final fixtureDef = FixtureDef(
|
||||||
|
shape,
|
||||||
|
restitution: 0.1,
|
||||||
|
);
|
||||||
|
final bodyDef = BodyDef(position: initialPosition);
|
||||||
|
|
||||||
|
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AndroidHeadSpriteAnimationComponent extends SpriteAnimationComponent
|
||||||
|
with HasGameRef {
|
||||||
|
_AndroidHeadSpriteAnimationComponent()
|
||||||
|
: super(
|
||||||
|
anchor: Anchor.center,
|
||||||
|
position: Vector2(-0.24, -2.6),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
|
||||||
|
final spriteSheet = gameRef.images.fromCache(
|
||||||
|
Assets.images.android.spaceship.animatronic.keyName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const amountPerRow = 18;
|
||||||
|
const amountPerColumn = 4;
|
||||||
|
final textureSize = Vector2(
|
||||||
|
spriteSheet.width / amountPerRow,
|
||||||
|
spriteSheet.height / amountPerColumn,
|
||||||
|
);
|
||||||
|
size = textureSize / 10;
|
||||||
|
|
||||||
|
animation = SpriteAnimation.fromFrameData(
|
||||||
|
spriteSheet,
|
||||||
|
SpriteAnimationData.sequenced(
|
||||||
|
amount: amountPerRow * amountPerColumn,
|
||||||
|
amountPerRow: amountPerRow,
|
||||||
|
stepTime: 1 / 24,
|
||||||
|
textureSize: textureSize,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpaceshipHole extends LayerSensor {
|
||||||
|
_SpaceshipHole({required Layer outsideLayer, required int outsidePriority})
|
||||||
|
: super(
|
||||||
|
insideLayer: Layer.spaceship,
|
||||||
|
outsideLayer: outsideLayer,
|
||||||
|
orientation: LayerEntranceOrientation.down,
|
||||||
|
insidePriority: RenderPriority.ballOnSpaceship,
|
||||||
|
outsidePriority: outsidePriority,
|
||||||
|
) {
|
||||||
|
layer = Layer.spaceship;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Shape get shape {
|
||||||
|
return ArcShape(
|
||||||
|
center: Vector2(0, -3.2),
|
||||||
|
arcRadius: 5,
|
||||||
|
angle: 1,
|
||||||
|
rotation: -2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
part 'multiplier_state.dart';
|
||||||
|
|
||||||
|
class MultiplierCubit extends Cubit<MultiplierState> {
|
||||||
|
MultiplierCubit(MultiplierValue multiplierValue)
|
||||||
|
: super(MultiplierState.initial(multiplierValue));
|
||||||
|
|
||||||
|
/// Event added when the game's current multiplier changes.
|
||||||
|
void next(int multiplier) {
|
||||||
|
if (state.value.equals(multiplier)) {
|
||||||
|
if (state.spriteState == MultiplierSpriteState.dimmed) {
|
||||||
|
emit(state.copyWith(spriteState: MultiplierSpriteState.lit));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (state.spriteState == MultiplierSpriteState.lit) {
|
||||||
|
emit(state.copyWith(spriteState: MultiplierSpriteState.dimmed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
part of 'multiplier_cubit.dart';
|
||||||
|
|
||||||
|
enum MultiplierSpriteState {
|
||||||
|
lit,
|
||||||
|
dimmed,
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultiplierState extends Equatable {
|
||||||
|
const MultiplierState({
|
||||||
|
required this.value,
|
||||||
|
required this.spriteState,
|
||||||
|
});
|
||||||
|
|
||||||
|
const MultiplierState.initial(MultiplierValue multiplierValue)
|
||||||
|
: this(
|
||||||
|
value: multiplierValue,
|
||||||
|
spriteState: MultiplierSpriteState.dimmed,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Current value for the [Multiplier]
|
||||||
|
final MultiplierValue value;
|
||||||
|
|
||||||
|
/// The [MultiplierSpriteGroupComponent] current sprite state
|
||||||
|
final MultiplierSpriteState spriteState;
|
||||||
|
|
||||||
|
MultiplierState copyWith({
|
||||||
|
MultiplierSpriteState? spriteState,
|
||||||
|
}) {
|
||||||
|
return MultiplierState(
|
||||||
|
value: value,
|
||||||
|
spriteState: spriteState ?? this.spriteState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [value, spriteState];
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MultiplierValueX on MultiplierValue {
|
||||||
|
bool equals(int value) {
|
||||||
|
switch (this) {
|
||||||
|
case MultiplierValue.x2:
|
||||||
|
return value == 2;
|
||||||
|
case MultiplierValue.x3:
|
||||||
|
return value == 3;
|
||||||
|
case MultiplierValue.x4:
|
||||||
|
return value == 4;
|
||||||
|
case MultiplierValue.x5:
|
||||||
|
return value == 5;
|
||||||
|
case MultiplierValue.x6:
|
||||||
|
return value == 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,204 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pinball_components/gen/assets.gen.dart';
|
||||||
|
import 'package:pinball_components/src/components/multiplier/cubit/multiplier_cubit.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
|
export 'cubit/multiplier_cubit.dart';
|
||||||
|
|
||||||
|
/// {@template multiplier}
|
||||||
|
/// Backlit multiplier decal displayed on the board.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class Multiplier extends Component {
|
||||||
|
/// {@macro multiplier}
|
||||||
|
Multiplier._({
|
||||||
|
required MultiplierValue value,
|
||||||
|
required Vector2 position,
|
||||||
|
required double angle,
|
||||||
|
required this.bloc,
|
||||||
|
}) : _value = value,
|
||||||
|
_position = position,
|
||||||
|
_angle = angle,
|
||||||
|
super();
|
||||||
|
|
||||||
|
/// {@macro multiplier}
|
||||||
|
Multiplier.x2({
|
||||||
|
required Vector2 position,
|
||||||
|
required double angle,
|
||||||
|
}) : this._(
|
||||||
|
value: MultiplierValue.x2,
|
||||||
|
position: position,
|
||||||
|
angle: angle,
|
||||||
|
bloc: MultiplierCubit(MultiplierValue.x2),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// {@macro multiplier}
|
||||||
|
Multiplier.x3({
|
||||||
|
required Vector2 position,
|
||||||
|
required double angle,
|
||||||
|
}) : this._(
|
||||||
|
value: MultiplierValue.x3,
|
||||||
|
position: position,
|
||||||
|
angle: angle,
|
||||||
|
bloc: MultiplierCubit(MultiplierValue.x3),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// {@macro multiplier}
|
||||||
|
Multiplier.x4({
|
||||||
|
required Vector2 position,
|
||||||
|
required double angle,
|
||||||
|
}) : this._(
|
||||||
|
value: MultiplierValue.x4,
|
||||||
|
position: position,
|
||||||
|
angle: angle,
|
||||||
|
bloc: MultiplierCubit(MultiplierValue.x4),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// {@macro multiplier}
|
||||||
|
Multiplier.x5({
|
||||||
|
required Vector2 position,
|
||||||
|
required double angle,
|
||||||
|
}) : this._(
|
||||||
|
value: MultiplierValue.x5,
|
||||||
|
position: position,
|
||||||
|
angle: angle,
|
||||||
|
bloc: MultiplierCubit(MultiplierValue.x5),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// {@macro multiplier}
|
||||||
|
Multiplier.x6({
|
||||||
|
required Vector2 position,
|
||||||
|
required double angle,
|
||||||
|
}) : this._(
|
||||||
|
value: MultiplierValue.x6,
|
||||||
|
position: position,
|
||||||
|
angle: angle,
|
||||||
|
bloc: MultiplierCubit(MultiplierValue.x6),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Creates a [Multiplier] without any children.
|
||||||
|
///
|
||||||
|
/// This can be used for testing [Multiplier]'s behaviors in isolation.
|
||||||
|
// TODO(alestiago): Refactor injecting bloc once the following is merged:
|
||||||
|
// https://github.com/flame-engine/flame/pull/1538
|
||||||
|
@visibleForTesting
|
||||||
|
Multiplier.test({
|
||||||
|
required MultiplierValue value,
|
||||||
|
required this.bloc,
|
||||||
|
}) : _value = value,
|
||||||
|
_position = Vector2.zero(),
|
||||||
|
_angle = 0;
|
||||||
|
|
||||||
|
// TODO(ruimiguel): Consider refactoring once the following is merged:
|
||||||
|
// https://github.com/flame-engine/flame/pull/1538
|
||||||
|
final MultiplierCubit bloc;
|
||||||
|
|
||||||
|
final MultiplierValue _value;
|
||||||
|
final Vector2 _position;
|
||||||
|
final double _angle;
|
||||||
|
late final MultiplierSpriteGroupComponent _sprite;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onRemove() {
|
||||||
|
bloc.close();
|
||||||
|
super.onRemove();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
_sprite = MultiplierSpriteGroupComponent(
|
||||||
|
position: _position,
|
||||||
|
litAssetPath: _value.litAssetPath,
|
||||||
|
dimmedAssetPath: _value.dimmedAssetPath,
|
||||||
|
angle: _angle,
|
||||||
|
current: bloc.state,
|
||||||
|
);
|
||||||
|
await add(_sprite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Available multiplier values.
|
||||||
|
enum MultiplierValue {
|
||||||
|
x2,
|
||||||
|
x3,
|
||||||
|
x4,
|
||||||
|
x5,
|
||||||
|
x6,
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on MultiplierValue {
|
||||||
|
String get litAssetPath {
|
||||||
|
switch (this) {
|
||||||
|
case MultiplierValue.x2:
|
||||||
|
return Assets.images.multiplier.x2.lit.keyName;
|
||||||
|
case MultiplierValue.x3:
|
||||||
|
return Assets.images.multiplier.x3.lit.keyName;
|
||||||
|
case MultiplierValue.x4:
|
||||||
|
return Assets.images.multiplier.x4.lit.keyName;
|
||||||
|
case MultiplierValue.x5:
|
||||||
|
return Assets.images.multiplier.x5.lit.keyName;
|
||||||
|
case MultiplierValue.x6:
|
||||||
|
return Assets.images.multiplier.x6.lit.keyName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get dimmedAssetPath {
|
||||||
|
switch (this) {
|
||||||
|
case MultiplierValue.x2:
|
||||||
|
return Assets.images.multiplier.x2.dimmed.keyName;
|
||||||
|
case MultiplierValue.x3:
|
||||||
|
return Assets.images.multiplier.x3.dimmed.keyName;
|
||||||
|
case MultiplierValue.x4:
|
||||||
|
return Assets.images.multiplier.x4.dimmed.keyName;
|
||||||
|
case MultiplierValue.x5:
|
||||||
|
return Assets.images.multiplier.x5.dimmed.keyName;
|
||||||
|
case MultiplierValue.x6:
|
||||||
|
return Assets.images.multiplier.x6.dimmed.keyName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template multiplier_sprite_group_component}
|
||||||
|
/// A [SpriteGroupComponent] for a [Multiplier] with lit and dimmed states.
|
||||||
|
/// {@endtemplate}
|
||||||
|
@visibleForTesting
|
||||||
|
class MultiplierSpriteGroupComponent
|
||||||
|
extends SpriteGroupComponent<MultiplierSpriteState>
|
||||||
|
with HasGameRef, ParentIsA<Multiplier> {
|
||||||
|
/// {@macro multiplier_sprite_group_component}
|
||||||
|
MultiplierSpriteGroupComponent({
|
||||||
|
required Vector2 position,
|
||||||
|
required String litAssetPath,
|
||||||
|
required String dimmedAssetPath,
|
||||||
|
required double angle,
|
||||||
|
required MultiplierState current,
|
||||||
|
}) : _litAssetPath = litAssetPath,
|
||||||
|
_dimmedAssetPath = dimmedAssetPath,
|
||||||
|
super(
|
||||||
|
anchor: Anchor.center,
|
||||||
|
position: position,
|
||||||
|
angle: angle,
|
||||||
|
current: current.spriteState,
|
||||||
|
);
|
||||||
|
|
||||||
|
final String _litAssetPath;
|
||||||
|
final String _dimmedAssetPath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
parent.bloc.stream.listen((state) => current = state.spriteState);
|
||||||
|
|
||||||
|
final sprites = {
|
||||||
|
MultiplierSpriteState.lit:
|
||||||
|
Sprite(gameRef.images.fromCache(_litAssetPath)),
|
||||||
|
MultiplierSpriteState.dimmed:
|
||||||
|
Sprite(gameRef.images.fromCache(_dimmedAssetPath)),
|
||||||
|
};
|
||||||
|
this.sprites = sprites;
|
||||||
|
size = sprites[current]!.originalSize / 10;
|
||||||
|
}
|
||||||
|
}
|
@ -1,246 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flame/components.dart';
|
|
||||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
|
||||||
import 'package:pinball_components/gen/assets.gen.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart' hide Assets;
|
|
||||||
import 'package:pinball_flame/pinball_flame.dart';
|
|
||||||
|
|
||||||
/// {@template spaceship}
|
|
||||||
/// A [Blueprint] which creates the spaceship feature.
|
|
||||||
/// {@endtemplate}
|
|
||||||
class Spaceship extends Blueprint {
|
|
||||||
/// {@macro spaceship}
|
|
||||||
Spaceship({required Vector2 position})
|
|
||||||
: super(
|
|
||||||
components: [
|
|
||||||
SpaceshipSaucer()..initialPosition = position,
|
|
||||||
_SpaceshipEntrance()..initialPosition = position,
|
|
||||||
AndroidHead()..initialPosition = position,
|
|
||||||
_SpaceshipHole(
|
|
||||||
outsideLayer: Layer.spaceshipExitRail,
|
|
||||||
outsidePriority: RenderPriority.ballOnSpaceshipRail,
|
|
||||||
)..initialPosition = position - Vector2(5.2, -4.8),
|
|
||||||
_SpaceshipHole(
|
|
||||||
outsideLayer: Layer.board,
|
|
||||||
outsidePriority: RenderPriority.ballOnBoard,
|
|
||||||
)..initialPosition = position - Vector2(-7.2, -0.8),
|
|
||||||
SpaceshipWall()..initialPosition = position,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Total size of the spaceship.
|
|
||||||
static final size = Vector2(25, 19);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template spaceship_saucer}
|
|
||||||
/// A [BodyComponent] for the base, or the saucer of the spaceship
|
|
||||||
/// {@endtemplate}
|
|
||||||
class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
|
|
||||||
/// {@macro spaceship_saucer}
|
|
||||||
SpaceshipSaucer()
|
|
||||||
: super(
|
|
||||||
priority: RenderPriority.spaceshipSaucer,
|
|
||||||
renderBody: false,
|
|
||||||
children: [
|
|
||||||
_SpaceshipSaucerSpriteComponent(),
|
|
||||||
],
|
|
||||||
) {
|
|
||||||
layer = Layer.spaceship;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Body createBody() {
|
|
||||||
final shape = CircleShape()..radius = 3;
|
|
||||||
final fixtureDef = FixtureDef(
|
|
||||||
shape,
|
|
||||||
isSensor: true,
|
|
||||||
);
|
|
||||||
final bodyDef = BodyDef(
|
|
||||||
position: initialPosition,
|
|
||||||
userData: this,
|
|
||||||
);
|
|
||||||
|
|
||||||
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SpaceshipSaucerSpriteComponent extends SpriteComponent with HasGameRef {
|
|
||||||
_SpaceshipSaucerSpriteComponent()
|
|
||||||
: super(
|
|
||||||
anchor: Anchor.center,
|
|
||||||
// TODO(alestiago): Refactor to use sprite orignial size instead.
|
|
||||||
size: Spaceship.size,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
await super.onLoad();
|
|
||||||
|
|
||||||
// TODO(alestiago): Use cached sprite.
|
|
||||||
sprite = await gameRef.loadSprite(
|
|
||||||
Assets.images.spaceship.saucer.keyName,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template spaceship_bridge}
|
|
||||||
/// A [BodyComponent] that provides both the collision and the rotation
|
|
||||||
/// animation for the bridge.
|
|
||||||
/// {@endtemplate}
|
|
||||||
class AndroidHead extends BodyComponent with InitialPosition, Layered {
|
|
||||||
/// {@macro spaceship_bridge}
|
|
||||||
AndroidHead()
|
|
||||||
: super(
|
|
||||||
priority: RenderPriority.androidHead,
|
|
||||||
children: [_AndroidHeadSpriteAnimation()],
|
|
||||||
renderBody: false,
|
|
||||||
) {
|
|
||||||
layer = Layer.spaceship;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Body createBody() {
|
|
||||||
final circleShape = CircleShape()..radius = 2;
|
|
||||||
|
|
||||||
final bodyDef = BodyDef(
|
|
||||||
position: initialPosition,
|
|
||||||
userData: this,
|
|
||||||
);
|
|
||||||
|
|
||||||
return world.createBody(bodyDef)
|
|
||||||
..createFixture(
|
|
||||||
FixtureDef(circleShape)..restitution = 0.4,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AndroidHeadSpriteAnimation extends SpriteAnimationComponent
|
|
||||||
with HasGameRef {
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
await super.onLoad();
|
|
||||||
|
|
||||||
final image = await gameRef.images.load(
|
|
||||||
Assets.images.spaceship.bridge.keyName,
|
|
||||||
);
|
|
||||||
size = Vector2(8.2, 10);
|
|
||||||
position = Vector2(0, -2);
|
|
||||||
anchor = Anchor.center;
|
|
||||||
|
|
||||||
final data = SpriteAnimationData.sequenced(
|
|
||||||
amount: 72,
|
|
||||||
amountPerRow: 24,
|
|
||||||
stepTime: 0.05,
|
|
||||||
textureSize: size * 10,
|
|
||||||
);
|
|
||||||
animation = SpriteAnimation.fromFrameData(image, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SpaceshipEntrance extends LayerSensor {
|
|
||||||
_SpaceshipEntrance()
|
|
||||||
: super(
|
|
||||||
insideLayer: Layer.spaceship,
|
|
||||||
orientation: LayerEntranceOrientation.up,
|
|
||||||
insidePriority: RenderPriority.ballOnSpaceship,
|
|
||||||
) {
|
|
||||||
layer = Layer.spaceship;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Shape get shape {
|
|
||||||
final radius = Spaceship.size.y / 2;
|
|
||||||
return PolygonShape()
|
|
||||||
..setAsEdge(
|
|
||||||
Vector2(
|
|
||||||
radius * cos(20 * pi / 180),
|
|
||||||
radius * sin(20 * pi / 180),
|
|
||||||
)..rotate(90 * pi / 180),
|
|
||||||
Vector2(
|
|
||||||
radius * cos(340 * pi / 180),
|
|
||||||
radius * sin(340 * pi / 180),
|
|
||||||
)..rotate(90 * pi / 180),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SpaceshipHole extends LayerSensor {
|
|
||||||
_SpaceshipHole({required Layer outsideLayer, required int outsidePriority})
|
|
||||||
: super(
|
|
||||||
insideLayer: Layer.spaceship,
|
|
||||||
outsideLayer: outsideLayer,
|
|
||||||
orientation: LayerEntranceOrientation.down,
|
|
||||||
insidePriority: RenderPriority.ballOnSpaceship,
|
|
||||||
outsidePriority: outsidePriority,
|
|
||||||
) {
|
|
||||||
layer = Layer.spaceship;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Shape get shape {
|
|
||||||
return ArcShape(
|
|
||||||
center: Vector2(0, -3.2),
|
|
||||||
arcRadius: 5,
|
|
||||||
angle: 1,
|
|
||||||
rotation: -2,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template spaceship_wall_shape}
|
|
||||||
/// The [ChainShape] that defines the shape of the [SpaceshipWall].
|
|
||||||
/// {@endtemplate}
|
|
||||||
class _SpaceshipWallShape extends ChainShape {
|
|
||||||
/// {@macro spaceship_wall_shape}
|
|
||||||
_SpaceshipWallShape() {
|
|
||||||
final minorRadius = (Spaceship.size.y - 2) / 2;
|
|
||||||
final majorRadius = (Spaceship.size.x - 2) / 2;
|
|
||||||
|
|
||||||
createChain(
|
|
||||||
[
|
|
||||||
// TODO(alestiago): Try converting this logic to radian.
|
|
||||||
for (var angle = 20; angle <= 340; angle++)
|
|
||||||
Vector2(
|
|
||||||
minorRadius * cos(angle * pi / 180),
|
|
||||||
majorRadius * sin(angle * pi / 180),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template spaceship_wall}
|
|
||||||
/// A [BodyComponent] that provides the collision for the wall
|
|
||||||
/// surrounding the spaceship.
|
|
||||||
///
|
|
||||||
/// It has a small opening to allow the [Ball] to get inside the spaceship
|
|
||||||
/// saucer.
|
|
||||||
///
|
|
||||||
/// It also contains the [SpriteComponent] for the lower wall
|
|
||||||
/// {@endtemplate}
|
|
||||||
class SpaceshipWall extends BodyComponent with InitialPosition, Layered {
|
|
||||||
/// {@macro spaceship_wall}
|
|
||||||
SpaceshipWall()
|
|
||||||
: super(
|
|
||||||
priority: RenderPriority.spaceshipSaucerWall,
|
|
||||||
renderBody: false,
|
|
||||||
) {
|
|
||||||
layer = Layer.spaceship;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Body createBody() {
|
|
||||||
final shape = _SpaceshipWallShape();
|
|
||||||
final fixtureDef = FixtureDef(shape);
|
|
||||||
|
|
||||||
final bodyDef = BodyDef(
|
|
||||||
position: initialPosition,
|
|
||||||
userData: this,
|
|
||||||
angle: -1.7,
|
|
||||||
);
|
|
||||||
|
|
||||||
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,33 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flame/extensions.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:sandbox/stories/ball/basic_ball_game.dart';
|
||||||
|
|
||||||
|
class AndroidBumperCowGame extends BallGame {
|
||||||
|
AndroidBumperCowGame()
|
||||||
|
: super(
|
||||||
|
imagesFileNames: [
|
||||||
|
Assets.images.android.bumper.cow.lit.keyName,
|
||||||
|
Assets.images.android.bumper.cow.dimmed.keyName,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
static const description = '''
|
||||||
|
Shows how a AndroidBumper.cow is rendered.
|
||||||
|
|
||||||
|
- Activate the "trace" parameter to overlay the body.
|
||||||
|
''';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
|
||||||
|
camera.followVector2(Vector2.zero());
|
||||||
|
await add(
|
||||||
|
AndroidBumper.cow()..priority = 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
await traceAllBodies();
|
||||||
|
}
|
||||||
|
}
|