mirror of https://github.com/flutter/pinball.git
feat: add alien bumper (#166)
* feat: added alien bumpers * test: tests for alien bumpers * feat: sandbox for alien bumpers * refactor: changed alien bumper ellipses * feat: added alien bumpers zone to game * test: tests for alien zone * refactor: final size and positions for alien bumpers * feat: added new alien zone bumpers * test: changed tests for alien * chore: api doc * refactor: alien sandbox traceable * Update lib/game/components/alien_zone.dart Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * refactor: moved alien from board to pinball and fixed sprites * test: fixed test for alien at pinball * Update packages/pinball_components/sandbox/lib/stories/alien_bumper/alien_bumper_game.dart Co-authored-by: Alejandro Santiago <dev@alestiago.com> * test: clean flamebloc test * refactor: ControlledAlienBumper visible for testing * chore: removed unused file * test: fixed alien test in pinball * refactor: refactored alien dashbook stories * chore: analysis error Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Co-authored-by: Alejandro Santiago <dev@alestiago.com>pull/186/head
parent
b424f0a008
commit
aafc254ad3
@ -0,0 +1,95 @@
|
|||||||
|
// ignore_for_file: avoid_renaming_method_parameters
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pinball/flame/flame.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
/// {@template alien_zone}
|
||||||
|
/// Area positioned below [Spaceship] where the [Ball]
|
||||||
|
/// can bounce off [AlienBumper]s.
|
||||||
|
///
|
||||||
|
/// When a [Ball] hits [AlienBumper]s, they toggle between activated and
|
||||||
|
/// deactivated states.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class AlienZone extends Component with HasGameRef<PinballGame> {
|
||||||
|
/// {@macro alien_zone}
|
||||||
|
AlienZone();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
|
||||||
|
gameRef.addContactCallback(_ControlledAlienBumperBallContactCallback());
|
||||||
|
|
||||||
|
final lowerBumper = ControlledAlienBumper.a()
|
||||||
|
..initialPosition = Vector2(-32.52, 9.34);
|
||||||
|
final upperBumper = ControlledAlienBumper.b()
|
||||||
|
..initialPosition = Vector2(-22.89, 17.43);
|
||||||
|
|
||||||
|
await addAll([
|
||||||
|
lowerBumper,
|
||||||
|
upperBumper,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template controlled_alien_bumper}
|
||||||
|
/// [AlienBumper] with [_AlienBumperController] attached.
|
||||||
|
/// {@endtemplate}
|
||||||
|
@visibleForTesting
|
||||||
|
class ControlledAlienBumper extends AlienBumper
|
||||||
|
with Controls<_AlienBumperController>, ScorePoints {
|
||||||
|
/// {@macro controlled_alien_bumper}
|
||||||
|
ControlledAlienBumper.a() : super.a() {
|
||||||
|
controller = _AlienBumperController(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@macro controlled_alien_bumper}
|
||||||
|
ControlledAlienBumper.b() : super.b() {
|
||||||
|
controller = _AlienBumperController(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
// TODO(ruimiguel): change points when get final points map.
|
||||||
|
int get points => 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template alien_bumper_controller}
|
||||||
|
/// Controls a [AlienBumper].
|
||||||
|
/// {@endtemplate}
|
||||||
|
class _AlienBumperController extends ComponentController<AlienBumper>
|
||||||
|
with HasGameRef<PinballGame> {
|
||||||
|
/// {@macro alien_bumper_controller}
|
||||||
|
_AlienBumperController(AlienBumper alienBumper) : super(alienBumper);
|
||||||
|
|
||||||
|
/// Flag for activated state of the [AlienBumper].
|
||||||
|
///
|
||||||
|
/// Used to toggle [AlienBumper]s' state between activated and deactivated.
|
||||||
|
bool isActivated = false;
|
||||||
|
|
||||||
|
/// Registers when a [AlienBumper] is hit by a [Ball].
|
||||||
|
void hit() {
|
||||||
|
if (isActivated) {
|
||||||
|
component.deactivate();
|
||||||
|
} else {
|
||||||
|
component.activate();
|
||||||
|
}
|
||||||
|
isActivated = !isActivated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Listens when a [Ball] bounces bounces against a [AlienBumper].
|
||||||
|
class _ControlledAlienBumperBallContactCallback
|
||||||
|
extends ContactCallback<Controls<_AlienBumperController>, Ball> {
|
||||||
|
@override
|
||||||
|
void begin(
|
||||||
|
Controls<_AlienBumperController> controlledAlienBumper,
|
||||||
|
Ball _,
|
||||||
|
Contact __,
|
||||||
|
) {
|
||||||
|
controlledAlienBumper.controller.hit();
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 8.2 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 4.5 KiB |
@ -0,0 +1,109 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
/// {@template alien_bumper}
|
||||||
|
/// Bumper for Alien area.
|
||||||
|
/// {@endtemplate}
|
||||||
|
// TODO(ruimiguel): refactor later to unify with DashBumpers.
|
||||||
|
class AlienBumper extends BodyComponent with InitialPosition {
|
||||||
|
/// {@macro alien_bumper}
|
||||||
|
AlienBumper._({
|
||||||
|
required double majorRadius,
|
||||||
|
required double minorRadius,
|
||||||
|
required String activeAssetPath,
|
||||||
|
required String inactiveAssetPath,
|
||||||
|
required SpriteComponent spriteComponent,
|
||||||
|
}) : _majorRadius = majorRadius,
|
||||||
|
_minorRadius = minorRadius,
|
||||||
|
_activeAssetPath = activeAssetPath,
|
||||||
|
_inactiveAssetPath = inactiveAssetPath,
|
||||||
|
_spriteComponent = spriteComponent;
|
||||||
|
|
||||||
|
/// {@macro alien_bumper}
|
||||||
|
AlienBumper.a()
|
||||||
|
: this._(
|
||||||
|
majorRadius: 3.52,
|
||||||
|
minorRadius: 2.97,
|
||||||
|
activeAssetPath: Assets.images.alienBumper.a.active.keyName,
|
||||||
|
inactiveAssetPath: Assets.images.alienBumper.a.inactive.keyName,
|
||||||
|
spriteComponent: SpriteComponent(
|
||||||
|
anchor: Anchor.center,
|
||||||
|
position: Vector2(0, -0.1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// {@macro alien_bumper}
|
||||||
|
AlienBumper.b()
|
||||||
|
: this._(
|
||||||
|
majorRadius: 3.19,
|
||||||
|
minorRadius: 2.79,
|
||||||
|
activeAssetPath: Assets.images.alienBumper.b.active.keyName,
|
||||||
|
inactiveAssetPath: Assets.images.alienBumper.b.inactive.keyName,
|
||||||
|
spriteComponent: SpriteComponent(
|
||||||
|
anchor: Anchor.center,
|
||||||
|
position: Vector2(0, -0.1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final double _majorRadius;
|
||||||
|
final double _minorRadius;
|
||||||
|
final String _activeAssetPath;
|
||||||
|
late final Sprite _activeSprite;
|
||||||
|
final String _inactiveAssetPath;
|
||||||
|
late final Sprite _inactiveSprite;
|
||||||
|
final SpriteComponent _spriteComponent;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
renderBody = false;
|
||||||
|
|
||||||
|
await _loadSprites();
|
||||||
|
|
||||||
|
deactivate();
|
||||||
|
await add(_spriteComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Body createBody() {
|
||||||
|
final shape = EllipseShape(
|
||||||
|
center: Vector2.zero(),
|
||||||
|
majorRadius: _majorRadius,
|
||||||
|
minorRadius: _minorRadius,
|
||||||
|
)..rotate(15.9 * math.pi / 180);
|
||||||
|
final fixtureDef = FixtureDef(shape)
|
||||||
|
..friction = 0
|
||||||
|
..restitution = 4;
|
||||||
|
|
||||||
|
final bodyDef = BodyDef()
|
||||||
|
..position = initialPosition
|
||||||
|
..userData = this;
|
||||||
|
|
||||||
|
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadSprites() async {
|
||||||
|
// TODO(alestiago): I think ideally we would like to do:
|
||||||
|
// Sprite(path).load so we don't require to store the activeAssetPath and
|
||||||
|
// the inactive assetPath.
|
||||||
|
_inactiveSprite = await gameRef.loadSprite(_inactiveAssetPath);
|
||||||
|
_activeSprite = await gameRef.loadSprite(_activeAssetPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Activates the [AlienBumper].
|
||||||
|
void activate() {
|
||||||
|
_spriteComponent
|
||||||
|
..sprite = _activeSprite
|
||||||
|
..size = _activeSprite.originalSize / 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deactivates the [AlienBumper].
|
||||||
|
void deactivate() {
|
||||||
|
_spriteComponent
|
||||||
|
..sprite = _inactiveSprite
|
||||||
|
..size = _inactiveSprite.originalSize / 10;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
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 AlienBumperAGame extends BasicBallGame {
|
||||||
|
AlienBumperAGame() : super(color: const Color(0xFF0000FF));
|
||||||
|
|
||||||
|
static const info = '''
|
||||||
|
Shows how a AlienBumperA is rendered.
|
||||||
|
|
||||||
|
- Activate the "trace" parameter to overlay the body.
|
||||||
|
''';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
|
||||||
|
final center = screenToWorld(camera.viewport.canvasSize! / 2);
|
||||||
|
final alienBumperA = AlienBumper.a()
|
||||||
|
..initialPosition = Vector2(center.x - 20, center.y - 20)
|
||||||
|
..priority = 1;
|
||||||
|
await add(alienBumperA);
|
||||||
|
|
||||||
|
await traceAllBodies();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
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 AlienBumperBGame extends BasicBallGame {
|
||||||
|
AlienBumperBGame() : super(color: const Color(0xFF0000FF));
|
||||||
|
|
||||||
|
static const info = '''
|
||||||
|
Shows how a AlienBumperB is rendered.
|
||||||
|
|
||||||
|
- Activate the "trace" parameter to overlay the body.
|
||||||
|
''';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
|
||||||
|
final center = screenToWorld(camera.viewport.canvasSize! / 2);
|
||||||
|
final alienBumperB = AlienBumper.b()
|
||||||
|
..initialPosition = Vector2(center.x - 10, center.y + 10)
|
||||||
|
..priority = 1;
|
||||||
|
await add(alienBumperB);
|
||||||
|
|
||||||
|
await traceAllBodies();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import 'package:dashbook/dashbook.dart';
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:sandbox/common/common.dart';
|
||||||
|
import 'package:sandbox/stories/alien_zone/alien_bumper_a_game.dart';
|
||||||
|
import 'package:sandbox/stories/alien_zone/alien_bumper_b_game.dart';
|
||||||
|
|
||||||
|
void addAlienZoneStories(Dashbook dashbook) {
|
||||||
|
dashbook.storiesOf('Alien Zone')
|
||||||
|
..add(
|
||||||
|
'Alien Bumper A',
|
||||||
|
(context) => GameWidget(
|
||||||
|
game: AlienBumperAGame()..trace = context.boolProperty('Trace', true),
|
||||||
|
),
|
||||||
|
codeLink: buildSourceLink('alien_zone/alien_bumper_a.dart'),
|
||||||
|
info: AlienBumperAGame.info,
|
||||||
|
)
|
||||||
|
..add(
|
||||||
|
'Alien Bumper B',
|
||||||
|
(context) => GameWidget(
|
||||||
|
game: AlienBumperBGame()..trace = context.boolProperty('Trace', true),
|
||||||
|
),
|
||||||
|
codeLink: buildSourceLink('alien_zone/alien_bumper_b.dart'),
|
||||||
|
info: AlienBumperAGame.info,
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_test/flame_test.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
import '../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final flameTester = FlameTester(TestGame.new);
|
||||||
|
|
||||||
|
group('AlienBumper', () {
|
||||||
|
flameTester.test('"a" loads correctly', (game) async {
|
||||||
|
final bumper = AlienBumper.a();
|
||||||
|
await game.ensureAdd(bumper);
|
||||||
|
|
||||||
|
expect(game.contains(bumper), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('"b" loads correctly', (game) async {
|
||||||
|
final bumper = AlienBumper.b();
|
||||||
|
await game.ensureAdd(bumper);
|
||||||
|
expect(game.contains(bumper), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('activate returns normally', (game) async {
|
||||||
|
final bumper = AlienBumper.a();
|
||||||
|
await game.ensureAdd(bumper);
|
||||||
|
|
||||||
|
expect(bumper.activate, returnsNormally);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('deactivate returns normally', (game) async {
|
||||||
|
final bumper = AlienBumper.a();
|
||||||
|
await game.ensureAdd(bumper);
|
||||||
|
|
||||||
|
expect(bumper.deactivate, returnsNormally);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('changes sprite', (game) async {
|
||||||
|
final bumper = AlienBumper.a();
|
||||||
|
await game.ensureAdd(bumper);
|
||||||
|
|
||||||
|
final spriteComponent = bumper.firstChild<SpriteComponent>()!;
|
||||||
|
|
||||||
|
final deactivatedSprite = spriteComponent.sprite;
|
||||||
|
bumper.activate();
|
||||||
|
expect(
|
||||||
|
spriteComponent.sprite,
|
||||||
|
isNot(equals(deactivatedSprite)),
|
||||||
|
);
|
||||||
|
|
||||||
|
final activatedSprite = spriteComponent.sprite;
|
||||||
|
bumper.deactivate();
|
||||||
|
expect(
|
||||||
|
spriteComponent.sprite,
|
||||||
|
isNot(equals(activatedSprite)),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
activatedSprite,
|
||||||
|
isNot(equals(deactivatedSprite)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:bloc_test/bloc_test.dart';
|
||||||
|
import 'package:flame_test/flame_test.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
import '../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final flameTester = FlameTester(EmptyPinballGameTest.new);
|
||||||
|
|
||||||
|
group('AlienZone', () {
|
||||||
|
flameTester.test(
|
||||||
|
'loads correctly',
|
||||||
|
(game) async {
|
||||||
|
await game.ready();
|
||||||
|
final alienZone = AlienZone();
|
||||||
|
await game.ensureAdd(alienZone);
|
||||||
|
|
||||||
|
expect(game.contains(alienZone), isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('loads', () {
|
||||||
|
flameTester.test(
|
||||||
|
'two AlienBumper',
|
||||||
|
(game) async {
|
||||||
|
await game.ready();
|
||||||
|
final alienZone = AlienZone();
|
||||||
|
await game.ensureAdd(alienZone);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
alienZone.descendants().whereType<AlienBumper>().length,
|
||||||
|
equals(2),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('bumpers', () {
|
||||||
|
late ControlledAlienBumper controlledAlienBumper;
|
||||||
|
late GameBloc gameBloc;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
gameBloc = MockGameBloc();
|
||||||
|
});
|
||||||
|
|
||||||
|
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
|
||||||
|
gameBuilder: EmptyPinballGameTest.new,
|
||||||
|
blocBuilder: () => gameBloc,
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.testGameWidget(
|
||||||
|
'activate when deactivated bumper is hit',
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
controlledAlienBumper = ControlledAlienBumper.a();
|
||||||
|
await game.ensureAdd(controlledAlienBumper);
|
||||||
|
|
||||||
|
controlledAlienBumper.controller.hit();
|
||||||
|
},
|
||||||
|
verify: (game, tester) async {
|
||||||
|
expect(controlledAlienBumper.controller.isActivated, isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.testGameWidget(
|
||||||
|
'deactivate when activated bumper is hit',
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
controlledAlienBumper = ControlledAlienBumper.a();
|
||||||
|
await game.ensureAdd(controlledAlienBumper);
|
||||||
|
|
||||||
|
controlledAlienBumper.controller.hit();
|
||||||
|
controlledAlienBumper.controller.hit();
|
||||||
|
},
|
||||||
|
verify: (game, tester) async {
|
||||||
|
expect(controlledAlienBumper.controller.isActivated, isFalse);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameBlocTester.testGameWidget(
|
||||||
|
'add Scored event',
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
final ball = Ball(baseColor: const Color(0xFF00FFFF));
|
||||||
|
final alienZone = AlienZone();
|
||||||
|
whenListen(
|
||||||
|
gameBloc,
|
||||||
|
const Stream<GameState>.empty(),
|
||||||
|
initialState: const GameState.initial(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await game.ensureAdd(alienZone);
|
||||||
|
await game.ensureAdd(ball);
|
||||||
|
game.addContactCallback(BallScorePointsCallback(game));
|
||||||
|
|
||||||
|
final bumpers = alienZone.descendants().whereType<ScorePoints>();
|
||||||
|
|
||||||
|
for (final bumper in bumpers) {
|
||||||
|
beginContact(game, bumper, ball);
|
||||||
|
verify(
|
||||||
|
() => gameBloc.add(
|
||||||
|
Scored(points: bumper.points),
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in new issue