Merge branch 'main' into feat/spaceship-drop-tube

pull/79/head
RuiAlonso 4 years ago
commit 4c4972bac5

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 MiB

@ -1,33 +1,64 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/flame/blueprint.dart'; import 'package:pinball/flame/blueprint.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template ball_type}
/// Specifies the type of [Ball].
///
/// Different [BallType]s are affected by different game mechanics.
/// {@endtemplate}
enum BallType {
/// A [Ball] spawned from the [Plunger].
///
/// [normal] balls decrease the [GameState.balls] when they fall through the
/// the [BottomWall].
normal,
/// A [Ball] that does not alter [GameState.balls].
///
/// For example, a [Ball] spawned by Dash in the [FlutterForest].
extra,
}
/// {@template ball_blueprint} /// {@template ball_blueprint}
/// [Blueprint] which cretes a ball game object /// [Blueprint] which cretes a ball game object.
/// {@endtemplate} /// {@endtemplate}
class BallBlueprint extends Blueprint<PinballGame> { class BallBlueprint extends Blueprint<PinballGame> {
/// {@macro ball_blueprint} /// {@macro ball_blueprint}
BallBlueprint({required this.position}); BallBlueprint({
required this.position,
required this.type,
});
/// The initial position of the [Ball] /// The initial position of the [Ball].
final Vector2 position; final Vector2 position;
/// {@macro ball_type}
final BallType type;
@override @override
void build(PinballGame gameRef) { void build(PinballGame gameRef) {
final baseColor = gameRef.theme.characterTheme.ballColor; final baseColor = gameRef.theme.characterTheme.ballColor;
final ball = Ball(baseColor: baseColor)..add(BallController()); final ball = Ball(baseColor: baseColor)
..add(
BallController(type: type),
);
add(ball..initialPosition = position + Vector2(0, ball.size.y / 2)); add(ball..initialPosition = position + Vector2(0, ball.size.y / 2));
} }
} }
/// {@template ball} /// {@template ball_controller}
/// A solid, [BodyType.dynamic] sphere that rolls and bounces along the /// Controller attached to a [Ball] that handles its game related logic.
/// [PinballGame].
/// {@endtemplate} /// {@endtemplate}
class BallController extends Component with HasGameRef<PinballGame> { class BallController extends Component with HasGameRef<PinballGame> {
/// {@macro ball_controller}
BallController({required this.type});
/// {@macro ball_type}
final BallType type;
/// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if /// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if
/// any are left. /// any are left.
/// ///
@ -35,9 +66,11 @@ class BallController extends Component with HasGameRef<PinballGame> {
/// a [BottomWall]. /// a [BottomWall].
void lost() { void lost() {
parent?.shouldRemove = true; parent?.shouldRemove = true;
// TODO(alestiago): Consider adding test for this logic once we remove the
// BallX extension.
if (type != BallType.normal) return;
final bloc = gameRef.read<GameBloc>()..add(const BallLost()); final bloc = gameRef.read<GameBloc>()..add(const BallLost());
final shouldBallRespwan = !bloc.state.isLastBall && !bloc.state.isGameOver; final shouldBallRespwan = !bloc.state.isLastBall && !bloc.state.isGameOver;
if (shouldBallRespwan) { if (shouldBallRespwan) {
gameRef.spawnBall(); gameRef.spawnBall();

@ -21,8 +21,16 @@ class Board extends Component {
final flutterForest = FlutterForest(); final flutterForest = FlutterForest();
// TODO(alestiago): adjust positioning to real design.
final dino = ChromeDino()
..initialPosition = Vector2(
PinballGame.boardBounds.center.dx + 25,
PinballGame.boardBounds.center.dy + 10,
);
await addAll([ await addAll([
bottomGroup, bottomGroup,
dino,
flutterForest, flutterForest,
]); ]);
} }

@ -0,0 +1,169 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart' hide Timer;
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template chrome_dino}
/// Dinosaur that gobbles up a [Ball], swivel his head around, and shoots it
/// back out.
/// {@endtemplate}
class ChromeDino extends BodyComponent with InitialPosition {
/// {@macro chrome_dino}
ChromeDino() {
// TODO(alestiago): Remove once sprites are defined.
paint = Paint()..color = Colors.blue;
}
/// The size of the dinosaur mouth.
static final size = Vector2(5, 2.5);
/// Anchors the [ChromeDino] to the [RevoluteJoint] that controls its arc
/// motion.
Future<_ChromeDinoJoint> _anchorToJoint() async {
final anchor = _ChromeDinoAnchor(chromeDino: this);
await add(anchor);
final jointDef = _ChromeDinoAnchorRevoluteJointDef(
chromeDino: this,
anchor: anchor,
);
final joint = _ChromeDinoJoint(jointDef);
world.createJoint2(joint);
return joint;
}
@override
Future<void> onLoad() async {
await super.onLoad();
final joint = await _anchorToJoint();
await add(
TimerComponent(
period: 1,
onTick: joint.swivel,
repeat: true,
),
);
}
List<FixtureDef> _createFixtureDefs() {
final fixtureDefs = <FixtureDef>[];
// TODO(alestiago): Subject to change when sprites are added.
final box = PolygonShape()..setAsBoxXY(size.x / 2, size.y / 2);
final fixtureDef = FixtureDef(box)
..shape = box
..density = 999
..friction = 0.3
..restitution = 0.1
..isSensor = true;
fixtureDefs.add(fixtureDef);
// FIXME(alestiago): Investigate why adding these fixtures is considered as
// an invalid contact type.
// final upperEdge = EdgeShape()
// ..set(
// Vector2(-size.x / 2, -size.y / 2),
// Vector2(size.x / 2, -size.y / 2),
// );
// final upperEdgeDef = FixtureDef(upperEdge)..density = 0.5;
// fixtureDefs.add(upperEdgeDef);
// final lowerEdge = EdgeShape()
// ..set(
// Vector2(-size.x / 2, size.y / 2),
// Vector2(size.x / 2, size.y / 2),
// );
// final lowerEdgeDef = FixtureDef(lowerEdge)..density = 0.5;
// fixtureDefs.add(lowerEdgeDef);
// final rightEdge = EdgeShape()
// ..set(
// Vector2(size.x / 2, -size.y / 2),
// Vector2(size.x / 2, size.y / 2),
// );
// final rightEdgeDef = FixtureDef(rightEdge)..density = 0.5;
// fixtureDefs.add(rightEdgeDef);
return fixtureDefs;
}
@override
Body createBody() {
final bodyDef = BodyDef()
..gravityScale = 0
..position = initialPosition
..type = BodyType.dynamic;
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
return body;
}
}
/// {@template flipper_anchor}
/// [JointAnchor] positioned at the end of a [ChromeDino].
/// {@endtemplate}
class _ChromeDinoAnchor extends JointAnchor {
/// {@macro flipper_anchor}
_ChromeDinoAnchor({
required ChromeDino chromeDino,
}) {
initialPosition = Vector2(
chromeDino.body.position.x + ChromeDino.size.x / 2,
chromeDino.body.position.y,
);
}
}
/// {@template chrome_dino_anchor_revolute_joint_def}
/// Hinges a [ChromeDino] to a [_ChromeDinoAnchor].
/// {@endtemplate}
class _ChromeDinoAnchorRevoluteJointDef extends RevoluteJointDef {
/// {@macro chrome_dino_anchor_revolute_joint_def}
_ChromeDinoAnchorRevoluteJointDef({
required ChromeDino chromeDino,
required _ChromeDinoAnchor anchor,
}) {
initialize(
chromeDino.body,
anchor.body,
anchor.body.position,
);
enableLimit = true;
// TODO(alestiago): Apply design angle value.
const angle = math.pi / 3.5;
lowerAngle = -angle / 2;
upperAngle = angle / 2;
enableMotor = true;
// TODO(alestiago): Tune this values.
maxMotorTorque = motorSpeed = chromeDino.body.mass * 30;
}
}
class _ChromeDinoJoint extends RevoluteJoint {
_ChromeDinoJoint(_ChromeDinoAnchorRevoluteJointDef def) : super(def);
/// Sweeps the [ChromeDino] up and down repeatedly.
void swivel() {
setMotorSpeed(-motorSpeed);
}
}
extension on World {
// TODO(alestiago): Remove once Forge2D supports custom joints.
void createJoint2(Joint joint) {
assert(!isLocked, '');
joints.add(joint);
joint.bodyA.joints.add(joint);
joint.bodyB.joints.add(joint);
}
}

@ -3,6 +3,7 @@ export 'baseboard.dart';
export 'board.dart'; export 'board.dart';
export 'board_side.dart'; export 'board_side.dart';
export 'bonus_word.dart'; export 'bonus_word.dart';
export 'chrome_dino.dart';
export 'flipper.dart'; export 'flipper.dart';
export 'flutter_forest.dart'; export 'flutter_forest.dart';
export 'jetpack_ramp.dart'; export 'jetpack_ramp.dart';

@ -35,6 +35,7 @@ class FlutterForest extends Component
gameRef.addFromBlueprint( gameRef.addFromBlueprint(
BallBlueprint( BallBlueprint(
position: Vector2(17.2, 52.7), position: Vector2(17.2, 52.7),
type: BallType.extra,
), ),
); );
} }

@ -9,6 +9,7 @@ extension PinballGameAssetsX on PinballGame {
await Future.wait([ await Future.wait([
images.load(components.Assets.images.ball.keyName), images.load(components.Assets.images.ball.keyName),
images.load(Assets.images.components.flipper.path), images.load(Assets.images.components.flipper.path),
images.load(Assets.images.components.background.path),
images.load(Assets.images.components.spaceship.androidTop.path), images.load(Assets.images.components.spaceship.androidTop.path),
images.load(Assets.images.components.spaceship.androidBottom.path), images.load(Assets.images.components.spaceship.androidBottom.path),
images.load(Assets.images.components.spaceship.lower.path), images.load(Assets.images.components.spaceship.lower.path),

@ -2,13 +2,15 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/extensions.dart'; import 'package:flame/extensions.dart';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/flame/blueprint.dart'; import 'package:pinball/flame/blueprint.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_theme/pinball_theme.dart' hide Assets;
class PinballGame extends Forge2DGame class PinballGame extends Forge2DGame
with FlameBloc, HasKeyboardHandlerComponents { with FlameBloc, HasKeyboardHandlerComponents {
@ -49,7 +51,7 @@ class PinballGame extends Forge2DGame
// Fix camera on the center of the board. // Fix camera on the center of the board.
camera camera
..followVector2(Vector2.zero()) ..followVector2(Vector2(0, -7.8))
..zoom = size.y / 16; ..zoom = size.y / 16;
} }
@ -87,15 +89,48 @@ class PinballGame extends Forge2DGame
} }
void spawnBall() { void spawnBall() {
addFromBlueprint(BallBlueprint(position: plunger.body.position)); addFromBlueprint(
BallBlueprint(
position: plunger.body.position,
type: BallType.normal,
),
);
} }
} }
class DebugPinballGame extends PinballGame with TapDetector { class DebugPinballGame extends PinballGame with TapDetector {
DebugPinballGame({required PinballTheme theme}) : super(theme: theme); DebugPinballGame({required PinballTheme theme}) : super(theme: theme);
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadBackground();
}
// TODO(alestiago): Move to PinballGame once we have the real background
// component.
Future<void> _loadBackground() async {
final sprite = await loadSprite(
Assets.images.components.background.path,
);
final spriteComponent = SpriteComponent(
sprite: sprite,
size: Vector2(120, 160),
anchor: Anchor.center,
)
..position = Vector2(0, -7.8)
..priority = -1;
await add(spriteComponent);
}
@override @override
void onTapUp(TapUpInfo info) { void onTapUp(TapUpInfo info) {
addFromBlueprint(BallBlueprint(position: info.eventPosition.game)); addFromBlueprint(
BallBlueprint(
position: info.eventPosition.game,
type: BallType.extra,
),
);
} }
} }

@ -3,6 +3,8 @@
/// FlutterGen /// FlutterGen
/// ***************************************************** /// *****************************************************
// ignore_for_file: directives_ordering,unnecessary_import
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class $AssetsImagesGen { class $AssetsImagesGen {
@ -15,8 +17,14 @@ class $AssetsImagesGen {
class $AssetsImagesComponentsGen { class $AssetsImagesComponentsGen {
const $AssetsImagesComponentsGen(); const $AssetsImagesComponentsGen();
/// File path: assets/images/components/background.png
AssetGenImage get background =>
const AssetGenImage('assets/images/components/background.png');
/// File path: assets/images/components/flipper.png
AssetGenImage get flipper => AssetGenImage get flipper =>
const AssetGenImage('assets/images/components/flipper.png'); const AssetGenImage('assets/images/components/flipper.png');
$AssetsImagesComponentsSpaceshipGen get spaceship => $AssetsImagesComponentsSpaceshipGen get spaceship =>
const $AssetsImagesComponentsSpaceshipGen(); const $AssetsImagesComponentsSpaceshipGen();
} }
@ -24,14 +32,23 @@ class $AssetsImagesComponentsGen {
class $AssetsImagesComponentsSpaceshipGen { class $AssetsImagesComponentsSpaceshipGen {
const $AssetsImagesComponentsSpaceshipGen(); const $AssetsImagesComponentsSpaceshipGen();
/// File path: assets/images/components/spaceship/android-bottom.png
AssetGenImage get androidBottom => const AssetGenImage( AssetGenImage get androidBottom => const AssetGenImage(
'assets/images/components/spaceship/android-bottom.png'); 'assets/images/components/spaceship/android-bottom.png');
/// File path: assets/images/components/spaceship/android-top.png
AssetGenImage get androidTop => AssetGenImage get androidTop =>
const AssetGenImage('assets/images/components/spaceship/android-top.png'); const AssetGenImage('assets/images/components/spaceship/android-top.png');
/// File path: assets/images/components/spaceship/lower.png
AssetGenImage get lower => AssetGenImage get lower =>
const AssetGenImage('assets/images/components/spaceship/lower.png'); const AssetGenImage('assets/images/components/spaceship/lower.png');
/// File path: assets/images/components/spaceship/saucer.png
AssetGenImage get saucer => AssetGenImage get saucer =>
const AssetGenImage('assets/images/components/spaceship/saucer.png'); const AssetGenImage('assets/images/components/spaceship/saucer.png');
/// File path: assets/images/components/spaceship/upper.png
AssetGenImage get upper => AssetGenImage get upper =>
const AssetGenImage('assets/images/components/spaceship/upper.png'); const AssetGenImage('assets/images/components/spaceship/upper.png');
} }

@ -86,6 +86,18 @@ void main() {
expect(flutterForest.length, equals(1)); expect(flutterForest.length, equals(1));
}, },
); );
flameTester.test(
'has one ChromeDino',
(game) async {
final board = Board();
await game.ready();
await game.ensureAdd(board);
final chromeDino = board.descendants().whereType<ChromeDino>();
expect(chromeDino.length, equals(1));
},
);
}); });
}); });
} }

@ -0,0 +1,23 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(Forge2DGame.new);
group('ChromeDino', () {
flameTester.test(
'loads correctly',
(game) async {
final chromeDino = ChromeDino();
await game.ready();
await game.ensureAdd(chromeDino);
expect(game.contains(chromeDino), isTrue);
},
);
});
}
Loading…
Cancel
Save