diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 4b57f1dd..2736a07a 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -6,6 +6,7 @@ import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/gen/assets.gen.dart'; @@ -18,7 +19,8 @@ class PinballGame extends Forge2DGame with FlameBloc, HasKeyboardHandlerComponents, - Controls<_GameBallsController> { + Controls<_GameBallsController>, + TapDetector { PinballGame({ required this.characterTheme, required this.audio, @@ -70,6 +72,61 @@ class PinballGame extends Forge2DGame controller.attachTo(launcher.components.whereType().first); await super.onLoad(); } + + BoardSide? focusedBoardSide; + + @override + void onTapDown(TapDownInfo info) { + if (info.raw.kind == PointerDeviceKind.touch) { + final rocket = children.whereType().first; + final bounds = rocket.topLeftPosition & rocket.size; + + // NOTE(wolfen): As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually. + if (bounds.contains(info.eventPosition.game.toOffset())) { + children.whereType().first.pull(); + } else { + final leftSide = info.eventPosition.widget.x < canvasSize.x / 2; + focusedBoardSide = leftSide ? BoardSide.left : BoardSide.right; + final flippers = descendants().whereType().where((flipper) { + return flipper.side == focusedBoardSide; + }); + flippers.first.moveUp(); + } + } + + super.onTapDown(info); + } + + @override + void onTapUp(TapUpInfo info) { + final rocket = descendants().whereType().first; + final bounds = rocket.topLeftPosition & rocket.size; + + if (bounds.contains(info.eventPosition.game.toOffset())) { + children.whereType().first.release(); + } else { + _moveFlippersDown(); + } + super.onTapUp(info); + } + + @override + void onTapCancel() { + children.whereType().first.release(); + + _moveFlippersDown(); + super.onTapCancel(); + } + + void _moveFlippersDown() { + if (focusedBoardSide != null) { + final flippers = descendants().whereType().where((flipper) { + return flipper.side == focusedBoardSide; + }); + flippers.first.moveDown(); + focusedBoardSide = null; + } + } } class _GameBallsController extends ComponentController @@ -116,7 +173,7 @@ class _GameBallsController extends ComponentController } } -class DebugPinballGame extends PinballGame with FPSCounter, TapDetector { +class DebugPinballGame extends PinballGame with FPSCounter { DebugPinballGame({ required CharacterTheme characterTheme, required PinballAudio audio, @@ -153,9 +210,11 @@ class DebugPinballGame extends PinballGame with FPSCounter, TapDetector { @override void onTapUp(TapUpInfo info) { - add( - ControlledBall.debug()..initialPosition = info.eventPosition.game, - ); + super.onTapUp(info); + + if (info.raw.kind == PointerDeviceKind.mouse) { + add(ControlledBall.debug()..initialPosition = info.eventPosition.game); + } } } diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 2fdbe6c4..2e585d66 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -3,6 +3,7 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame_test/flame_test.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; @@ -229,6 +230,181 @@ void main() { ); }); }); + + group('flipper control', () { + flameTester.test('tap down moves left flipper up', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(Vector2.zero()); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.left, + ); + + game.onTapDown(tapDownEvent); + + expect(flippers.first.body.linearVelocity.y, isNegative); + }); + + flameTester.test('tap down moves right flipper up', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(game.canvasSize); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.right, + ); + + game.onTapDown(tapDownEvent); + + expect(flippers.first.body.linearVelocity.y, isNegative); + }); + + flameTester.test('tap up moves flipper down', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(Vector2.zero()); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.left, + ); + + game.onTapDown(tapDownEvent); + + expect(flippers.first.body.linearVelocity.y, isNegative); + + final tapUpEvent = MockTapUpInfo(); + when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + + game.onTapUp(tapUpEvent); + await game.ready(); + + expect(flippers.first.body.linearVelocity.y, isPositive); + }); + + flameTester.test('tap cancel moves flipper down', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(Vector2.zero()); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.left, + ); + + game.onTapDown(tapDownEvent); + + expect(flippers.first.body.linearVelocity.y, isNegative); + + game.onTapCancel(); + + expect(flippers.first.body.linearVelocity.y, isPositive); + }); + }); + + group('plunger control', () { + flameTester.test('tap down moves plunger down', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2(40, 60)); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final plunger = game.descendants().whereType().first; + + game.onTapDown(tapDownEvent); + + expect(plunger.body.linearVelocity.y, equals(7)); + }); + + flameTester.test('tap up releases plunger', (game) async { + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2(40, 60)); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final plunger = game.descendants().whereType().first; + game.onTapDown(tapDownEvent); + + expect(plunger.body.linearVelocity.y, equals(7)); + + final tapUpEvent = MockTapUpInfo(); + when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + + game.onTapUp(tapUpEvent); + + expect(plunger.body.linearVelocity.y, equals(0)); + }); + + flameTester.test('tap cancel releases plunger', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2(40, 60)); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final plunger = game.descendants().whereType().first; + game.onTapDown(tapDownEvent); + + expect(plunger.body.linearVelocity.y, equals(7)); + + game.onTapCancel(); + + expect(plunger.body.linearVelocity.y, equals(0)); + }); + }); }); group('DebugPinballGame', () { @@ -238,8 +414,12 @@ void main() { final eventPosition = MockEventPosition(); when(() => eventPosition.game).thenReturn(Vector2.all(10)); + final raw = MockTapUpDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.mouse); + final tapUpEvent = MockTapUpInfo(); when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + when(() => tapUpEvent.raw).thenReturn(raw); final previousBalls = game.descendants().whereType().toList(); diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index b58dc619..ad999aff 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -2,7 +2,7 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.dart'; @@ -55,8 +55,14 @@ class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { } } +class MockTapDownInfo extends Mock implements TapDownInfo {} + +class MockTapDownDetails extends Mock implements TapDownDetails {} + class MockTapUpInfo extends Mock implements TapUpInfo {} +class MockTapUpDetails extends Mock implements TapUpDetails {} + class MockEventPosition extends Mock implements EventPosition {} class MockFilter extends Mock implements Filter {}