|
After Width: | Height: | Size: 6.2 KiB |
@ -0,0 +1,35 @@
|
|||||||
|
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';
|
||||||
|
import 'package:pinball_theme/pinball_theme.dart';
|
||||||
|
|
||||||
|
/// Spawns a new [Ball] into the game when all balls are lost and still
|
||||||
|
/// [GameStatus.playing].
|
||||||
|
class BallSpawningBehavior extends Component
|
||||||
|
with FlameBlocListenable<GameBloc, GameState>, HasGameRef {
|
||||||
|
@override
|
||||||
|
bool listenWhen(GameState? previousState, GameState newState) {
|
||||||
|
if (!newState.status.isPlaying) return false;
|
||||||
|
|
||||||
|
final startedGame = previousState?.status.isWaiting ?? true;
|
||||||
|
final lostRound =
|
||||||
|
(previousState?.rounds ?? newState.rounds + 1) > newState.rounds;
|
||||||
|
return startedGame || lostRound;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onNewState(GameState state) {
|
||||||
|
final plunger = gameRef.descendants().whereType<Plunger>().single;
|
||||||
|
final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single;
|
||||||
|
final characterTheme = readProvider<CharacterTheme>();
|
||||||
|
final ball = ControlledBall.launch(characterTheme: characterTheme)
|
||||||
|
..initialPosition = Vector2(
|
||||||
|
plunger.body.position.x,
|
||||||
|
plunger.body.position.y - Ball.size.y,
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.add(ball);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,2 +1,4 @@
|
|||||||
export 'bumper_noisy_behavior.dart';
|
export 'ball_spawning_behavior.dart';
|
||||||
|
export 'bumper_noise_behavior.dart';
|
||||||
|
export 'camera_focusing_behavior.dart';
|
||||||
export 'scoring_behavior.dart';
|
export 'scoring_behavior.dart';
|
||||||
|
|||||||
@ -1,15 +1,13 @@
|
|||||||
// ignore_for_file: public_member_api_docs
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
import 'package:flame/components.dart';
|
|
||||||
import 'package:flame_forge2d/flame_forge2d.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_audio/pinball_audio.dart';
|
||||||
import 'package:pinball_flame/pinball_flame.dart';
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
class BumperNoisyBehavior extends ContactBehavior with HasGameRef<PinballGame> {
|
class BumperNoiseBehavior extends ContactBehavior {
|
||||||
@override
|
@override
|
||||||
void beginContact(Object other, Contact contact) {
|
void beginContact(Object other, Contact contact) {
|
||||||
super.beginContact(other, contact);
|
super.beginContact(other, contact);
|
||||||
gameRef.player.play(PinballAudio.bumper);
|
readProvider<PinballPlayer>().play(PinballAudio.bumper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:flame_bloc/flame_bloc.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
/// {@template focus_data}
|
||||||
|
/// Defines a [Camera] focus point.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class FocusData {
|
||||||
|
/// {@template focus_data}
|
||||||
|
FocusData({
|
||||||
|
required this.zoom,
|
||||||
|
required this.position,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The amount of zoom.
|
||||||
|
final double zoom;
|
||||||
|
|
||||||
|
/// The position of the camera.
|
||||||
|
final Vector2 position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changes the game focus when the [GameBloc] status changes.
|
||||||
|
class CameraFocusingBehavior extends Component
|
||||||
|
with FlameBlocListenable<GameBloc, GameState>, HasGameRef {
|
||||||
|
late final Map<String, FocusData> _foci;
|
||||||
|
|
||||||
|
@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:
|
||||||
|
_zoom(_foci['game']!);
|
||||||
|
break;
|
||||||
|
case GameStatus.gameOver:
|
||||||
|
_zoom(_foci['backbox']!);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
_foci = {
|
||||||
|
'game': FocusData(
|
||||||
|
zoom: gameRef.size.y / 16,
|
||||||
|
position: Vector2(0, -7.8),
|
||||||
|
),
|
||||||
|
'waiting': FocusData(
|
||||||
|
zoom: gameRef.size.y / 18,
|
||||||
|
position: Vector2(0, -112),
|
||||||
|
),
|
||||||
|
'backbox': FocusData(
|
||||||
|
zoom: gameRef.size.y / 10,
|
||||||
|
position: Vector2(0, -111),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
_snap(_foci['waiting']!);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _snap(FocusData data) {
|
||||||
|
gameRef.camera
|
||||||
|
..speed = 100
|
||||||
|
..followVector2(data.position)
|
||||||
|
..zoom = data.zoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _zoom(FocusData data) {
|
||||||
|
final zoom = CameraZoom(value: data.zoom);
|
||||||
|
zoom.completed.then((_) {
|
||||||
|
gameRef.camera.moveTo(data.position);
|
||||||
|
});
|
||||||
|
add(zoom);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
export 'initials_input_display.dart';
|
export 'initials_input_display.dart';
|
||||||
export 'initials_submission_failure_display.dart';
|
export 'initials_submission_failure_display.dart';
|
||||||
export 'initials_submission_success_display.dart';
|
export 'initials_submission_success_display.dart';
|
||||||
|
export 'leaderboard_display.dart';
|
||||||
export 'loading_display.dart';
|
export 'loading_display.dart';
|
||||||
|
|||||||
@ -0,0 +1,120 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
||||||
|
import 'package:pinball/l10n/l10n.dart';
|
||||||
|
import 'package:pinball/leaderboard/models/leader_board_entry.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
import 'package:pinball_ui/pinball_ui.dart';
|
||||||
|
|
||||||
|
final _titleTextPaint = TextPaint(
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 2,
|
||||||
|
color: PinballColors.red,
|
||||||
|
fontFamily: PinballFonts.pixeloidSans,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final _bodyTextPaint = TextPaint(
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 1.8,
|
||||||
|
color: PinballColors.white,
|
||||||
|
fontFamily: PinballFonts.pixeloidSans,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// {@template leaderboard_display}
|
||||||
|
/// Component that builds the leaderboard list of the Backbox.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class LeaderboardDisplay extends PositionComponent with HasGameRef {
|
||||||
|
/// {@macro leaderboard_display}
|
||||||
|
LeaderboardDisplay({required List<LeaderboardEntryData> entries})
|
||||||
|
: _entries = entries;
|
||||||
|
|
||||||
|
final List<LeaderboardEntryData> _entries;
|
||||||
|
|
||||||
|
double _calcY(int i) => (i * 3.2) + 3.2;
|
||||||
|
|
||||||
|
static const _columns = [-15.0, 0.0, 15.0];
|
||||||
|
|
||||||
|
String _rank(int number) {
|
||||||
|
switch (number) {
|
||||||
|
case 1:
|
||||||
|
return '${number}st';
|
||||||
|
case 2:
|
||||||
|
return '${number}nd';
|
||||||
|
case 3:
|
||||||
|
return '${number}rd';
|
||||||
|
default:
|
||||||
|
return '${number}th';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
position = Vector2(0, -30);
|
||||||
|
|
||||||
|
final l10n = readProvider<AppLocalizations>();
|
||||||
|
final ranking = _entries.take(5).toList();
|
||||||
|
await add(
|
||||||
|
PositionComponent(
|
||||||
|
position: Vector2(0, 4),
|
||||||
|
children: [
|
||||||
|
PositionComponent(
|
||||||
|
children: [
|
||||||
|
TextComponent(
|
||||||
|
text: l10n.rank,
|
||||||
|
textRenderer: _titleTextPaint,
|
||||||
|
position: Vector2(_columns[0], 0),
|
||||||
|
anchor: Anchor.center,
|
||||||
|
),
|
||||||
|
TextComponent(
|
||||||
|
text: l10n.score,
|
||||||
|
textRenderer: _titleTextPaint,
|
||||||
|
position: Vector2(_columns[1], 0),
|
||||||
|
anchor: Anchor.center,
|
||||||
|
),
|
||||||
|
TextComponent(
|
||||||
|
text: l10n.name,
|
||||||
|
textRenderer: _titleTextPaint,
|
||||||
|
position: Vector2(_columns[2], 0),
|
||||||
|
anchor: Anchor.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
for (var i = 0; i < ranking.length; i++)
|
||||||
|
PositionComponent(
|
||||||
|
children: [
|
||||||
|
TextComponent(
|
||||||
|
text: _rank(i + 1),
|
||||||
|
textRenderer: _bodyTextPaint,
|
||||||
|
position: Vector2(_columns[0], _calcY(i)),
|
||||||
|
anchor: Anchor.center,
|
||||||
|
),
|
||||||
|
TextComponent(
|
||||||
|
text: ranking[i].score.formatScore(),
|
||||||
|
textRenderer: _bodyTextPaint,
|
||||||
|
position: Vector2(_columns[1], _calcY(i)),
|
||||||
|
anchor: Anchor.center,
|
||||||
|
),
|
||||||
|
SpriteComponent.fromImage(
|
||||||
|
gameRef.images.fromCache(
|
||||||
|
ranking[i].character.toTheme.leaderboardIcon.keyName,
|
||||||
|
),
|
||||||
|
anchor: Anchor.center,
|
||||||
|
size: Vector2(1.8, 1.8),
|
||||||
|
position: Vector2(_columns[2] - 2.5, _calcY(i) + .25),
|
||||||
|
),
|
||||||
|
TextComponent(
|
||||||
|
text: ranking[i].playerInitials,
|
||||||
|
textRenderer: _bodyTextPaint,
|
||||||
|
position: Vector2(_columns[2] + 1, _calcY(i)),
|
||||||
|
anchor: Anchor.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,93 +0,0 @@
|
|||||||
import 'package:flame/components.dart';
|
|
||||||
import 'package:flame/game.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart';
|
|
||||||
import 'package:pinball_flame/pinball_flame.dart';
|
|
||||||
|
|
||||||
/// Adds helpers methods to Flame's [Camera].
|
|
||||||
extension CameraX on Camera {
|
|
||||||
/// Instantly apply the point of focus to the [Camera].
|
|
||||||
void snapToFocus(FocusData data) {
|
|
||||||
followVector2(data.position);
|
|
||||||
zoom = data.zoom;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a [CameraZoom] that can be added to a [FlameGame].
|
|
||||||
CameraZoom focusToCameraZoom(FocusData data) {
|
|
||||||
final zoom = CameraZoom(value: data.zoom);
|
|
||||||
zoom.completed.then((_) {
|
|
||||||
moveTo(data.position);
|
|
||||||
});
|
|
||||||
return zoom;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template focus_data}
|
|
||||||
/// Model class that defines a focus point of the camera.
|
|
||||||
/// {@endtemplate}
|
|
||||||
class FocusData {
|
|
||||||
/// {@template focus_data}
|
|
||||||
FocusData({
|
|
||||||
required this.zoom,
|
|
||||||
required this.position,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The amount of zoom.
|
|
||||||
final double zoom;
|
|
||||||
|
|
||||||
/// The position of the camera.
|
|
||||||
final Vector2 position;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template camera_controller}
|
|
||||||
/// A [Component] that controls its game camera focus.
|
|
||||||
/// {@endtemplate}
|
|
||||||
class CameraController extends ComponentController<FlameGame> {
|
|
||||||
/// {@macro camera_controller}
|
|
||||||
CameraController(FlameGame component) : super(component) {
|
|
||||||
final gameZoom = component.size.y / 16;
|
|
||||||
final waitingBackboxZoom = component.size.y / 18;
|
|
||||||
final gameOverBackboxZoom = component.size.y / 10;
|
|
||||||
|
|
||||||
gameFocus = FocusData(
|
|
||||||
zoom: gameZoom,
|
|
||||||
position: Vector2(0, -7.8),
|
|
||||||
);
|
|
||||||
waitingBackboxFocus = FocusData(
|
|
||||||
zoom: waitingBackboxZoom,
|
|
||||||
position: Vector2(0, -112),
|
|
||||||
);
|
|
||||||
gameOverBackboxFocus = FocusData(
|
|
||||||
zoom: gameOverBackboxZoom,
|
|
||||||
position: Vector2(0, -111),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Game starts with the camera focused on the [Backbox].
|
|
||||||
component.camera
|
|
||||||
..speed = 100
|
|
||||||
..snapToFocus(waitingBackboxFocus);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Holds the data for the game focus point.
|
|
||||||
late final FocusData gameFocus;
|
|
||||||
|
|
||||||
/// Holds the data for the waiting backbox focus point.
|
|
||||||
late final FocusData waitingBackboxFocus;
|
|
||||||
|
|
||||||
/// Holds the data for the game over backbox focus point.
|
|
||||||
late final FocusData gameOverBackboxFocus;
|
|
||||||
|
|
||||||
/// Move the camera focus to the game board.
|
|
||||||
void focusOnGame() {
|
|
||||||
component.add(component.camera.focusToCameraZoom(gameFocus));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Move the camera focus to the waiting backbox.
|
|
||||||
void focusOnWaitingBackbox() {
|
|
||||||
component.add(component.camera.focusToCameraZoom(waitingBackboxFocus));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Move the camera focus to the game over backbox.
|
|
||||||
void focusOnGameOverBackbox() {
|
|
||||||
component.add(component.camera.focusToCameraZoom(gameOverBackboxFocus));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
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 'draining_behavior.dart';
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_bloc/flame_bloc.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';
|
||||||
|
|
||||||
|
/// Handles removing a [Ball] from the game.
|
||||||
|
class DrainingBehavior extends ContactBehavior<Drain> with HasGameRef {
|
||||||
|
@override
|
||||||
|
void beginContact(Object other, Contact contact) {
|
||||||
|
super.beginContact(other, contact);
|
||||||
|
if (other is! Ball) return;
|
||||||
|
|
||||||
|
other.removeFromParent();
|
||||||
|
final ballsLeft = gameRef.descendants().whereType<Ball>().length;
|
||||||
|
if (ballsLeft - 1 == 0) {
|
||||||
|
ancestors()
|
||||||
|
.whereType<FlameBlocProvider<GameBloc, GameState>>()
|
||||||
|
.first
|
||||||
|
.bloc
|
||||||
|
.add(const RoundLost());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import 'package:flame/extensions.dart';
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:pinball/game/components/drain/behaviors/behaviors.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
/// {@template drain}
|
||||||
|
/// Area located at the bottom of the board.
|
||||||
|
///
|
||||||
|
/// Its [DrainingBehavior] handles removing a [Ball] from the game.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class Drain extends BodyComponent with ContactCallbacks {
|
||||||
|
/// {@macro drain}
|
||||||
|
Drain()
|
||||||
|
: super(
|
||||||
|
renderBody: false,
|
||||||
|
children: [DrainingBehavior()],
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Creates a [Drain] without any children.
|
||||||
|
///
|
||||||
|
/// This can be used for testing a [Drain]'s behaviors in isolation.
|
||||||
|
@visibleForTesting
|
||||||
|
Drain.test();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Body createBody() {
|
||||||
|
final shape = EdgeShape()
|
||||||
|
..set(
|
||||||
|
BoardDimensions.bounds.bottomLeft.toVector2(),
|
||||||
|
BoardDimensions.bounds.bottomRight.toVector2(),
|
||||||
|
);
|
||||||
|
final fixtureDef = FixtureDef(shape, isSensor: true);
|
||||||
|
return world.createBody(BodyDef())..createFixture(fixtureDef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 569 KiB After Width: | Height: | Size: 374 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 252 KiB After Width: | Height: | Size: 254 KiB |
@ -0,0 +1,65 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
|
||||||
|
class FlameProvider<T> extends Component {
|
||||||
|
FlameProvider.value(
|
||||||
|
this.provider, {
|
||||||
|
Iterable<Component>? children,
|
||||||
|
}) : super(
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
final T provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultiFlameProvider extends Component {
|
||||||
|
MultiFlameProvider({
|
||||||
|
required List<FlameProvider<dynamic>> providers,
|
||||||
|
Iterable<Component>? children,
|
||||||
|
}) : _providers = providers,
|
||||||
|
_initialChildren = children,
|
||||||
|
assert(providers.isNotEmpty, 'At least one provider must be given') {
|
||||||
|
_addProviders();
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<FlameProvider<dynamic>> _providers;
|
||||||
|
final Iterable<Component>? _initialChildren;
|
||||||
|
FlameProvider<dynamic>? _lastProvider;
|
||||||
|
|
||||||
|
Future<void> _addProviders() async {
|
||||||
|
final _list = [..._providers];
|
||||||
|
|
||||||
|
var current = _list.removeAt(0);
|
||||||
|
while (_list.isNotEmpty) {
|
||||||
|
final provider = _list.removeAt(0);
|
||||||
|
await current.add(provider);
|
||||||
|
current = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
await add(_providers.first);
|
||||||
|
_lastProvider = current;
|
||||||
|
|
||||||
|
_initialChildren?.forEach(add);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> add(Component component) async {
|
||||||
|
if (_lastProvider == null) {
|
||||||
|
await super.add(component);
|
||||||
|
}
|
||||||
|
await _lastProvider?.add(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ReadFlameProvider on Component {
|
||||||
|
T readProvider<T>() {
|
||||||
|
final providers = ancestors().whereType<FlameProvider<T>>();
|
||||||
|
assert(
|
||||||
|
providers.isNotEmpty,
|
||||||
|
'No FlameProvider<$T> available on the component tree',
|
||||||
|
);
|
||||||
|
|
||||||
|
return providers.first.provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:flame_test/flame_test.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final flameTester = FlameTester(FlameGame.new);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'FlameProvider',
|
||||||
|
() {
|
||||||
|
test('can be instantiated', () {
|
||||||
|
expect(
|
||||||
|
FlameProvider<bool>.value(true),
|
||||||
|
isA<FlameProvider<bool>>(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('can be loaded', (game) async {
|
||||||
|
final component = FlameProvider<bool>.value(true);
|
||||||
|
await game.ensureAdd(component);
|
||||||
|
expect(game.children, contains(component));
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('adds children', (game) async {
|
||||||
|
final component = Component();
|
||||||
|
final provider = FlameProvider<bool>.value(
|
||||||
|
true,
|
||||||
|
children: [component],
|
||||||
|
);
|
||||||
|
await game.ensureAdd(provider);
|
||||||
|
expect(provider.children, contains(component));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('MultiFlameProvider', () {
|
||||||
|
test('can be instantiated', () {
|
||||||
|
expect(
|
||||||
|
MultiFlameProvider(
|
||||||
|
providers: [
|
||||||
|
FlameProvider<bool>.value(true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
isA<MultiFlameProvider>(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('adds multiple providers', (game) async {
|
||||||
|
final provider1 = FlameProvider<bool>.value(true);
|
||||||
|
final provider2 = FlameProvider<bool>.value(true);
|
||||||
|
final providers = MultiFlameProvider(
|
||||||
|
providers: [provider1, provider2],
|
||||||
|
);
|
||||||
|
await game.ensureAdd(providers);
|
||||||
|
expect(providers.children, contains(provider1));
|
||||||
|
expect(provider1.children, contains(provider2));
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('adds children under provider', (game) async {
|
||||||
|
final component = Component();
|
||||||
|
final provider = FlameProvider<bool>.value(true);
|
||||||
|
final providers = MultiFlameProvider(
|
||||||
|
providers: [provider],
|
||||||
|
children: [component],
|
||||||
|
);
|
||||||
|
await game.ensureAdd(providers);
|
||||||
|
expect(provider.children, contains(component));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group(
|
||||||
|
'ReadFlameProvider',
|
||||||
|
() {
|
||||||
|
flameTester.test('loads provider', (game) async {
|
||||||
|
final component = Component();
|
||||||
|
final provider = FlameProvider<bool>.value(
|
||||||
|
true,
|
||||||
|
children: [component],
|
||||||
|
);
|
||||||
|
await game.ensureAdd(provider);
|
||||||
|
expect(component.readProvider<bool>(), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'throws assertionError when no provider is found',
|
||||||
|
(game) async {
|
||||||
|
final component = Component();
|
||||||
|
await game.ensureAdd(component);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => component.readProvider<bool>(),
|
||||||
|
throwsAssertionError,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,143 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:flame_bloc/flame_bloc.dart';
|
||||||
|
import 'package:flame_test/flame_test.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball/game/behaviors/camera_focusing_behavior.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group(
|
||||||
|
'CameraFocusingBehavior',
|
||||||
|
() {
|
||||||
|
final flameTester = FlameTester(FlameGame.new);
|
||||||
|
|
||||||
|
test('can be instantiated', () {
|
||||||
|
expect(
|
||||||
|
CameraFocusingBehavior(),
|
||||||
|
isA<CameraFocusingBehavior>(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('loads', (game) async {
|
||||||
|
late final behavior = CameraFocusingBehavior();
|
||||||
|
await game.ensureAdd(
|
||||||
|
FlameBlocProvider<GameBloc, GameState>.value(
|
||||||
|
value: GameBloc(),
|
||||||
|
children: [behavior],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(game.descendants(), contains(behavior));
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'changes focus when loaded',
|
||||||
|
(game) async {
|
||||||
|
final behavior = CameraFocusingBehavior();
|
||||||
|
final previousZoom = game.camera.zoom;
|
||||||
|
expect(game.camera.follow, isNull);
|
||||||
|
|
||||||
|
await game.ensureAdd(
|
||||||
|
FlameBlocProvider<GameBloc, GameState>.value(
|
||||||
|
value: GameBloc(),
|
||||||
|
children: [behavior],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(game.camera.follow, isNotNull);
|
||||||
|
expect(game.camera.zoom, isNot(equals(previousZoom)));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'listenWhen only listens when status changes',
|
||||||
|
(game) async {
|
||||||
|
final behavior = CameraFocusingBehavior();
|
||||||
|
const waiting = GameState.initial();
|
||||||
|
final playing =
|
||||||
|
const GameState.initial().copyWith(status: GameStatus.playing);
|
||||||
|
final gameOver =
|
||||||
|
const GameState.initial().copyWith(status: GameStatus.gameOver);
|
||||||
|
|
||||||
|
expect(behavior.listenWhen(waiting, waiting), isFalse);
|
||||||
|
expect(behavior.listenWhen(waiting, playing), isTrue);
|
||||||
|
expect(behavior.listenWhen(waiting, gameOver), isTrue);
|
||||||
|
|
||||||
|
expect(behavior.listenWhen(playing, playing), isFalse);
|
||||||
|
expect(behavior.listenWhen(playing, waiting), isTrue);
|
||||||
|
expect(behavior.listenWhen(playing, gameOver), isTrue);
|
||||||
|
|
||||||
|
expect(behavior.listenWhen(gameOver, gameOver), isFalse);
|
||||||
|
expect(behavior.listenWhen(gameOver, waiting), isTrue);
|
||||||
|
expect(behavior.listenWhen(gameOver, playing), isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('onNewState', () {
|
||||||
|
flameTester.test(
|
||||||
|
'zooms when started playing',
|
||||||
|
(game) async {
|
||||||
|
final playing =
|
||||||
|
const GameState.initial().copyWith(status: GameStatus.playing);
|
||||||
|
|
||||||
|
final behavior = CameraFocusingBehavior();
|
||||||
|
await game.ensureAdd(
|
||||||
|
FlameBlocProvider<GameBloc, GameState>.value(
|
||||||
|
value: GameBloc(),
|
||||||
|
children: [behavior],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
behavior.onNewState(playing);
|
||||||
|
final previousPosition = game.camera.position.clone();
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
final zoom = behavior.children.whereType<CameraZoom>().single;
|
||||||
|
game.update(zoom.controller.duration!);
|
||||||
|
game.update(0);
|
||||||
|
|
||||||
|
expect(zoom.controller.completed, isTrue);
|
||||||
|
expect(
|
||||||
|
game.camera.position,
|
||||||
|
isNot(equals(previousPosition)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'zooms when game is over',
|
||||||
|
(game) async {
|
||||||
|
final playing = const GameState.initial().copyWith(
|
||||||
|
status: GameStatus.gameOver,
|
||||||
|
);
|
||||||
|
|
||||||
|
final behavior = CameraFocusingBehavior();
|
||||||
|
await game.ensureAdd(
|
||||||
|
FlameBlocProvider<GameBloc, GameState>.value(
|
||||||
|
value: GameBloc(),
|
||||||
|
children: [behavior],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
behavior.onNewState(playing);
|
||||||
|
final previousPosition = game.camera.position.clone();
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
final zoom = behavior.children.whereType<CameraZoom>().single;
|
||||||
|
game.update(zoom.controller.duration!);
|
||||||
|
game.update(0);
|
||||||
|
|
||||||
|
expect(zoom.controller.completed, isTrue);
|
||||||
|
expect(
|
||||||
|
game.camera.position,
|
||||||
|
isNot(equals(previousPosition)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,142 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_bloc/flame_bloc.dart';
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:flame_test/flame_test.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:pinball/game/behaviors/ball_spawning_behavior.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
import 'package:pinball_theme/pinball_theme.dart' as theme;
|
||||||
|
|
||||||
|
class _TestGame extends Forge2DGame {
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
images.prefix = '';
|
||||||
|
await images.load(theme.Assets.images.dash.ball.keyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pump(
|
||||||
|
Iterable<Component> children, {
|
||||||
|
GameBloc? gameBloc,
|
||||||
|
}) async {
|
||||||
|
await ensureAdd(
|
||||||
|
FlameBlocProvider<GameBloc, GameState>.value(
|
||||||
|
value: gameBloc ?? GameBloc(),
|
||||||
|
children: [
|
||||||
|
FlameProvider<theme.CharacterTheme>.value(
|
||||||
|
const theme.DashTheme(),
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MockGameState extends Mock implements GameState {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group(
|
||||||
|
'BallSpawningBehavior',
|
||||||
|
() {
|
||||||
|
final flameTester = FlameTester(_TestGame.new);
|
||||||
|
|
||||||
|
test('can be instantiated', () {
|
||||||
|
expect(
|
||||||
|
BallSpawningBehavior(),
|
||||||
|
isA<BallSpawningBehavior>(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'loads',
|
||||||
|
(game) async {
|
||||||
|
final behavior = BallSpawningBehavior();
|
||||||
|
await game.pump([behavior]);
|
||||||
|
expect(game.descendants(), contains(behavior));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('listenWhen', () {
|
||||||
|
test(
|
||||||
|
'never listens when new state not playing',
|
||||||
|
() {
|
||||||
|
final waiting = const GameState.initial()
|
||||||
|
..copyWith(status: GameStatus.waiting);
|
||||||
|
final gameOver = const GameState.initial()
|
||||||
|
..copyWith(status: GameStatus.gameOver);
|
||||||
|
|
||||||
|
final behavior = BallSpawningBehavior();
|
||||||
|
expect(behavior.listenWhen(_MockGameState(), waiting), isFalse);
|
||||||
|
expect(behavior.listenWhen(_MockGameState(), gameOver), isFalse);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'listens when started playing',
|
||||||
|
() {
|
||||||
|
final waiting =
|
||||||
|
const GameState.initial().copyWith(status: GameStatus.waiting);
|
||||||
|
final playing =
|
||||||
|
const GameState.initial().copyWith(status: GameStatus.playing);
|
||||||
|
|
||||||
|
final behavior = BallSpawningBehavior();
|
||||||
|
expect(behavior.listenWhen(waiting, playing), isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'listens when lost rounds',
|
||||||
|
() {
|
||||||
|
final playing1 = const GameState.initial().copyWith(
|
||||||
|
status: GameStatus.playing,
|
||||||
|
rounds: 2,
|
||||||
|
);
|
||||||
|
final playing2 = const GameState.initial().copyWith(
|
||||||
|
status: GameStatus.playing,
|
||||||
|
rounds: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
final behavior = BallSpawningBehavior();
|
||||||
|
expect(behavior.listenWhen(playing1, playing2), isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"doesn't listen when didn't lose any rounds",
|
||||||
|
() {
|
||||||
|
final playing = const GameState.initial().copyWith(
|
||||||
|
status: GameStatus.playing,
|
||||||
|
rounds: 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
final behavior = BallSpawningBehavior();
|
||||||
|
expect(behavior.listenWhen(playing, playing), isFalse);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'onNewState adds a ball',
|
||||||
|
(game) async {
|
||||||
|
final behavior = BallSpawningBehavior();
|
||||||
|
await game.pump([
|
||||||
|
behavior,
|
||||||
|
ZCanvasComponent(),
|
||||||
|
Plunger.test(compressionDistance: 10),
|
||||||
|
]);
|
||||||
|
expect(game.descendants().whereType<Ball>(), isEmpty);
|
||||||
|
|
||||||
|
behavior.onNewState(_MockGameState());
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
expect(game.descendants().whereType<Ball>(), isNotEmpty);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||