diff --git a/lib/game/behaviors/scoring_behavior.dart b/lib/game/behaviors/scoring_behavior.dart index 84597838..eddcb580 100644 --- a/lib/game/behaviors/scoring_behavior.dart +++ b/lib/game/behaviors/scoring_behavior.dart @@ -40,13 +40,14 @@ class ScoringBehavior extends Component with HasGameRef { @override Future onLoad() async { gameRef.read().add(Scored(points: _points.value)); - await gameRef.firstChild()!.add( - ScoreComponent( - points: _points, - position: _position, - effectController: _effectController, - ), - ); + final canvas = gameRef.descendants().whereType().single; + await canvas.add( + ScoreComponent( + points: _points, + position: _position, + effectController: _effectController, + ), + ); } } diff --git a/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart index 8f1b46e8..c06e6f87 100644 --- a/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart +++ b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart @@ -17,7 +17,7 @@ class FlutterForestBonusBehavior extends Component final bumpers = parent.children.whereType(); final signpost = parent.firstChild()!; final animatronic = parent.firstChild()!; - final canvas = gameRef.firstChild()!; + final canvas = gameRef.descendants().whereType().single; for (final bumper in bumpers) { // TODO(alestiago): Refactor subscription management once the following is diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index bdf23759..907687c9 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -72,14 +72,23 @@ class PinballGame extends PinballForge2DGame ]; await add( - ZCanvasComponent( + CanvasComponent( + onSpritePainted: (paint) { + if (paint.filterQuality != FilterQuality.medium) { + paint.filterQuality = FilterQuality.medium; + } + }, children: [ - ...machine, - ...decals, - ...characterAreas, - Drain(), - BottomGroup(), - Launcher(), + ZCanvasComponent( + children: [ + ...machine, + ...decals, + ...characterAreas, + Drain(), + BottomGroup(), + Launcher(), + ], + ), ], ), ); @@ -169,7 +178,7 @@ class _GameBallsController extends ComponentController plunger.body.position.x, plunger.body.position.y - Ball.size.y, ); - component.firstChild()?.add(ball); + component.descendants().whereType().single.add(ball); }); } } @@ -203,9 +212,10 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { super.onTapUp(pointerId, info); if (info.raw.kind == PointerDeviceKind.mouse) { + final canvas = descendants().whereType().single; final ball = ControlledBall.debug() ..initialPosition = info.eventPosition.game; - firstChild()?.add(ball); + canvas.add(ball); } } @@ -230,10 +240,11 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { } void _turboChargeBall(Vector2 line) { + final canvas = descendants().whereType().single; final ball = ControlledBall.debug()..initialPosition = lineStart!; final impulse = line * -1 * 10; ball.add(BallTurboChargingBehavior(impulse: impulse)); - firstChild()?.add(ball); + canvas.add(ball); } } diff --git a/packages/pinball_flame/lib/pinball_flame.dart b/packages/pinball_flame/lib/pinball_flame.dart index 8d458574..6f8a40f7 100644 --- a/packages/pinball_flame/lib/pinball_flame.dart +++ b/packages/pinball_flame/lib/pinball_flame.dart @@ -1,9 +1,9 @@ library pinball_flame; +export 'src/canvas/canvas.dart'; export 'src/component_controller.dart'; export 'src/contact_behavior.dart'; export 'src/keyboard_input_controller.dart'; export 'src/parent_is_a.dart'; export 'src/pinball_forge2d_game.dart'; export 'src/sprite_animation.dart'; -export 'src/z_canvas_component.dart'; diff --git a/packages/pinball_flame/lib/src/canvas/canvas.dart b/packages/pinball_flame/lib/src/canvas/canvas.dart new file mode 100644 index 00000000..9c0c7a70 --- /dev/null +++ b/packages/pinball_flame/lib/src/canvas/canvas.dart @@ -0,0 +1,2 @@ +export 'canvas_component.dart'; +export 'z_canvas_component.dart'; diff --git a/packages/pinball_flame/lib/src/canvas/canvas_component.dart b/packages/pinball_flame/lib/src/canvas/canvas_component.dart new file mode 100644 index 00000000..ca6e64d0 --- /dev/null +++ b/packages/pinball_flame/lib/src/canvas/canvas_component.dart @@ -0,0 +1,47 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:pinball_flame/src/canvas/canvas_wrapper.dart'; + +/// Called right before [Canvas.drawImageRect] is called. +/// +/// This is useful since [Sprite.render] uses [Canvas.drawImageRect] to draw +/// the [Sprite]. +typedef PaintFunction = void Function(Paint); + +/// {@template canvas_component} +/// Allows listening before the rendering of [Sprite]s. +/// +/// The existance of this class is to hack around the fact that Flame doesn't +/// provide a global way to modify the default [Paint] before rendering a +/// [Sprite]. +/// {@endtemplate} +class CanvasComponent extends Component { + /// {@macro canvas_component} + CanvasComponent({ + PaintFunction? onSpritePainted, + Iterable? children, + }) : _canvas = _Canvas(onSpritePainted: onSpritePainted), + super(children: children); + + final _Canvas _canvas; + + @override + void renderTree(Canvas canvas) { + _canvas.canvas = canvas; + super.renderTree(_canvas); + } +} + +class _Canvas extends CanvasWrapper { + _Canvas({PaintFunction? onSpritePainted}) + : _onSpritePainted = onSpritePainted; + + final PaintFunction? _onSpritePainted; + + @override + void drawImageRect(Image image, Rect src, Rect dst, Paint paint) { + _onSpritePainted?.call(paint); + super.drawImageRect(image, src, dst, paint); + } +} diff --git a/packages/pinball_flame/lib/src/z_canvas_component.dart b/packages/pinball_flame/lib/src/canvas/canvas_wrapper.dart similarity index 65% rename from packages/pinball_flame/lib/src/z_canvas_component.dart rename to packages/pinball_flame/lib/src/canvas/canvas_wrapper.dart index 911c3e93..883527d2 100644 --- a/packages/pinball_flame/lib/src/z_canvas_component.dart +++ b/packages/pinball_flame/lib/src/canvas/canvas_wrapper.dart @@ -1,85 +1,11 @@ +// ignore_for_file: public_member_api_docs + import 'dart:typed_data'; import 'dart:ui'; -import 'package:flame/components.dart'; - -/// {@template z_canvas_component} -/// Draws [ZIndex] components after the all non-[ZIndex] components have been -/// drawn. -/// {@endtemplate} -class ZCanvasComponent extends Component { - /// {@macro z_canvas_component} - ZCanvasComponent({ - Iterable? children, - }) : _zCanvas = ZCanvas(), - super(children: children); - - final ZCanvas _zCanvas; - - @override - void renderTree(Canvas canvas) { - _zCanvas.canvas = canvas; - super.renderTree(_zCanvas); - _zCanvas.render(); - } -} - -/// Apply to any [Component] that will be rendered according to a -/// [ZIndex.zIndex]. -/// -/// [ZIndex] components must be descendants of a [ZCanvasComponent]. -/// -/// {@macro z_canvas.render} -mixin ZIndex on Component { - /// The z-index of this component. - /// - /// The higher the value, the later the component will be drawn. Hence, - /// rendering in front of [Component]s with lower [zIndex] values. - int zIndex = 0; - - @override - void renderTree( - Canvas canvas, - ) { - if (canvas is ZCanvas) { - canvas.buffer(this); - } else { - super.renderTree(canvas); - } - } -} - -/// The [ZCanvas] allows to postpone the rendering of [ZIndex] components. -/// -/// You should not use this class directly. -class ZCanvas implements Canvas { - /// The [Canvas] to render to. - /// - /// This is set by [ZCanvasComponent] when rendering. +class CanvasWrapper implements Canvas { late Canvas canvas; - final List _zBuffer = []; - - /// Postpones the rendering of [ZIndex] component and its children. - void buffer(ZIndex component) => _zBuffer.add(component); - - /// Renders all [ZIndex] components and their children. - /// - /// {@template z_canvas.render} - /// The rendering order is defined by the parent [ZIndex]. The children of - /// the same parent are rendered in the order they were added. - /// - /// If two [Component]s ever overlap each other, and have the same - /// [ZIndex.zIndex], there is no guarantee that the first one will be rendered - /// before the second one. - /// {@endtemplate} - void render() => _zBuffer - ..sort((a, b) => a.zIndex.compareTo(b.zIndex)) - ..whereType().forEach(_render) - ..clear(); - - void _render(Component component) => component.renderTree(canvas); - @override void clipPath(Path path, {bool doAntiAlias = true}) => canvas.clipPath(path, doAntiAlias: doAntiAlias); diff --git a/packages/pinball_flame/lib/src/canvas/z_canvas_component.dart b/packages/pinball_flame/lib/src/canvas/z_canvas_component.dart new file mode 100644 index 00000000..e097f359 --- /dev/null +++ b/packages/pinball_flame/lib/src/canvas/z_canvas_component.dart @@ -0,0 +1,77 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:pinball_flame/src/canvas/canvas_wrapper.dart'; + +/// {@template z_canvas_component} +/// Draws [ZIndex] components after the all non-[ZIndex] components have been +/// drawn. +/// {@endtemplate} +class ZCanvasComponent extends Component { + /// {@macro z_canvas_component} + ZCanvasComponent({ + Iterable? children, + }) : _zCanvas = _ZCanvas(), + super(children: children); + + final _ZCanvas _zCanvas; + + @override + void renderTree(Canvas canvas) { + _zCanvas.canvas = canvas; + super.renderTree(_zCanvas); + _zCanvas.render(); + } +} + +/// Apply to any [Component] that will be rendered according to a +/// [ZIndex.zIndex]. +/// +/// [ZIndex] components must be descendants of a [ZCanvasComponent]. +/// +/// {@macro z_canvas.render} +mixin ZIndex on Component { + /// The z-index of this component. + /// + /// The higher the value, the later the component will be drawn. Hence, + /// rendering in front of [Component]s with lower [zIndex] values. + int zIndex = 0; + + @override + void renderTree( + Canvas canvas, + ) { + if (canvas is _ZCanvas) { + canvas.buffer(this); + } else { + super.renderTree(canvas); + } + } +} + +/// The [_ZCanvas] allows to postpone the rendering of [ZIndex] components. +/// +/// You should not use this class directly. +class _ZCanvas extends CanvasWrapper { + final List _zBuffer = []; + + /// Postpones the rendering of [ZIndex] component and its children. + void buffer(ZIndex component) => _zBuffer.add(component); + + /// Renders all [ZIndex] components and their children. + /// + /// {@template z_canvas.render} + /// The rendering order is defined by the parent [ZIndex]. The children of + /// the same parent are rendered in the order they were added. + /// + /// If two [Component]s ever overlap each other, and have the same + /// [ZIndex.zIndex], there is no guarantee that the first one will be rendered + /// before the second one. + /// {@endtemplate} + void render() => _zBuffer + ..sort((a, b) => a.zIndex.compareTo(b.zIndex)) + ..whereType().forEach(_render) + ..clear(); + + void _render(Component component) => component.renderTree(canvas); +} diff --git a/packages/pinball_flame/test/src/canvas/canvas_component_test.dart b/packages/pinball_flame/test/src/canvas/canvas_component_test.dart new file mode 100644 index 00000000..7bf7fd88 --- /dev/null +++ b/packages/pinball_flame/test/src/canvas/canvas_component_test.dart @@ -0,0 +1,144 @@ +// ignore_for_file: cascade_invocations + +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui'; + +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/src/canvas/canvas_component.dart'; + +class _TestSpriteComponent extends SpriteComponent {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('CanvasComponent', () { + final flameTester = FlameTester(FlameGame.new); + + test('can be instantiated', () { + expect( + CanvasComponent(), + isA(), + ); + }); + + flameTester.test('loads correctly', (game) async { + final component = CanvasComponent(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + + flameTester.test( + 'adds children', + (game) async { + final component = Component(); + final canvas = CanvasComponent( + onSpritePainted: (paint) => paint.filterQuality = FilterQuality.high, + children: [component], + ); + + await game.ensureAdd(canvas); + + expect( + canvas.children.contains(component), + isTrue, + ); + }, + ); + + flameTester.testGameWidget( + 'calls onSpritePainted when paiting a sprite', + setUp: (game, tester) async { + final spriteComponent = _TestSpriteComponent(); + + final completer = Completer(); + decodeImageFromList( + Uint8List.fromList(_image), + completer.complete, + ); + spriteComponent.sprite = Sprite(await completer.future); + + var calls = 0; + final canvas = CanvasComponent( + onSpritePainted: (paint) => calls++, + children: [spriteComponent], + ); + + await game.ensureAdd(canvas); + await tester.pump(); + + expect(calls, equals(1)); + }, + ); + }); +} + +const List _image = [ + 0x89, + 0x50, + 0x4E, + 0x47, + 0x0D, + 0x0A, + 0x1A, + 0x0A, + 0x00, + 0x00, + 0x00, + 0x0D, + 0x49, + 0x48, + 0x44, + 0x52, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x01, + 0x08, + 0x06, + 0x00, + 0x00, + 0x00, + 0x1F, + 0x15, + 0xC4, + 0x89, + 0x00, + 0x00, + 0x00, + 0x0A, + 0x49, + 0x44, + 0x41, + 0x54, + 0x78, + 0x9C, + 0x63, + 0x00, + 0x01, + 0x00, + 0x00, + 0x05, + 0x00, + 0x01, + 0x0D, + 0x0A, + 0x2D, + 0xB4, + 0x00, + 0x00, + 0x00, + 0x00, + 0x49, + 0x45, + 0x4E, + 0x44, + 0xAE, +]; diff --git a/packages/pinball_flame/test/src/canvas/canvas_wrapper_test.dart b/packages/pinball_flame/test/src/canvas/canvas_wrapper_test.dart new file mode 100644 index 00000000..58da1ecd --- /dev/null +++ b/packages/pinball_flame/test/src/canvas/canvas_wrapper_test.dart @@ -0,0 +1,353 @@ +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter/material.dart' hide Image; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_flame/src/canvas/canvas_wrapper.dart'; + +class _MockCanvas extends Mock implements Canvas {} + +class _MockImage extends Mock implements Image {} + +class _MockPicture extends Mock implements Picture {} + +class _MockParagraph extends Mock implements Paragraph {} + +class _MockVertices extends Mock implements Vertices {} + +void main() { + group('CanvasWrapper', () { + group('CanvasWrapper', () { + late Canvas canvas; + late Path path; + late RRect rRect; + late Rect rect; + late Paint paint; + late Image atlas; + late BlendMode blendMode; + late Color color; + late Offset offset; + late Float64List float64list; + late Float32List float32list; + late Int32List int32list; + late Picture picture; + late Paragraph paragraph; + late Vertices vertices; + + setUp(() { + canvas = _MockCanvas(); + path = Path(); + rRect = RRect.zero; + rect = Rect.zero; + paint = Paint(); + atlas = _MockImage(); + blendMode = BlendMode.clear; + color = Colors.black; + offset = Offset.zero; + float64list = Float64List(1); + float32list = Float32List(1); + int32list = Int32List(1); + picture = _MockPicture(); + paragraph = _MockParagraph(); + vertices = _MockVertices(); + }); + + test("clipPath calls Canvas's clipPath", () { + CanvasWrapper() + ..canvas = canvas + ..clipPath(path, doAntiAlias: false); + verify( + () => canvas.clipPath(path, doAntiAlias: false), + ).called(1); + }); + + test("clipRRect calls Canvas's clipRRect", () { + CanvasWrapper() + ..canvas = canvas + ..clipRRect(rRect, doAntiAlias: false); + verify( + () => canvas.clipRRect(rRect, doAntiAlias: false), + ).called(1); + }); + + test("clipRect calls Canvas's clipRect", () { + CanvasWrapper() + ..canvas = canvas + ..clipRect(rect, doAntiAlias: false); + verify( + () => canvas.clipRect(rect, doAntiAlias: false), + ).called(1); + }); + + test("drawArc calls Canvas's drawArc", () { + CanvasWrapper() + ..canvas = canvas + ..drawArc(rect, 0, 1, false, paint); + verify( + () => canvas.drawArc(rect, 0, 1, false, paint), + ).called(1); + }); + + test("drawAtlas calls Canvas's drawAtlas", () { + CanvasWrapper() + ..canvas = canvas + ..drawAtlas(atlas, [], [], [], blendMode, rect, paint); + verify( + () => canvas.drawAtlas(atlas, [], [], [], blendMode, rect, paint), + ).called(1); + }); + + test("drawCircle calls Canvas's drawCircle", () { + CanvasWrapper() + ..canvas = canvas + ..drawCircle(offset, 0, paint); + verify( + () => canvas.drawCircle(offset, 0, paint), + ).called(1); + }); + + test("drawColor calls Canvas's drawColor", () { + CanvasWrapper() + ..canvas = canvas + ..drawColor(color, blendMode); + verify( + () => canvas.drawColor(color, blendMode), + ).called(1); + }); + + test("drawDRRect calls Canvas's drawDRRect", () { + CanvasWrapper() + ..canvas = canvas + ..drawDRRect(rRect, rRect, paint); + verify( + () => canvas.drawDRRect(rRect, rRect, paint), + ).called(1); + }); + + test("drawImage calls Canvas's drawImage", () { + CanvasWrapper() + ..canvas = canvas + ..drawImage(atlas, offset, paint); + verify( + () => canvas.drawImage(atlas, offset, paint), + ).called(1); + }); + + test("drawImageNine calls Canvas's drawImageNine", () { + CanvasWrapper() + ..canvas = canvas + ..drawImageNine(atlas, rect, rect, paint); + verify( + () => canvas.drawImageNine(atlas, rect, rect, paint), + ).called(1); + }); + + test("drawImageRect calls Canvas's drawImageRect", () { + CanvasWrapper() + ..canvas = canvas + ..drawImageRect(atlas, rect, rect, paint); + verify( + () => canvas.drawImageRect(atlas, rect, rect, paint), + ).called(1); + }); + + test("drawLine calls Canvas's drawLine", () { + CanvasWrapper() + ..canvas = canvas + ..drawLine(offset, offset, paint); + verify( + () => canvas.drawLine(offset, offset, paint), + ).called(1); + }); + + test("drawOval calls Canvas's drawOval", () { + CanvasWrapper() + ..canvas = canvas + ..drawOval(rect, paint); + verify( + () => canvas.drawOval(rect, paint), + ).called(1); + }); + + test("drawPaint calls Canvas's drawPaint", () { + CanvasWrapper() + ..canvas = canvas + ..drawPaint(paint); + verify( + () => canvas.drawPaint(paint), + ).called(1); + }); + + test("drawParagraph calls Canvas's drawParagraph", () { + CanvasWrapper() + ..canvas = canvas + ..drawParagraph(paragraph, offset); + verify( + () => canvas.drawParagraph(paragraph, offset), + ).called(1); + }); + + test("drawPath calls Canvas's drawPath", () { + CanvasWrapper() + ..canvas = canvas + ..drawPath(path, paint); + verify( + () => canvas.drawPath(path, paint), + ).called(1); + }); + + test("drawPicture calls Canvas's drawPicture", () { + CanvasWrapper() + ..canvas = canvas + ..drawPicture(picture); + verify( + () => canvas.drawPicture(picture), + ).called(1); + }); + + test("drawPoints calls Canvas's drawPoints", () { + CanvasWrapper() + ..canvas = canvas + ..drawPoints(PointMode.points, [offset], paint); + verify( + () => canvas.drawPoints(PointMode.points, [offset], paint), + ).called(1); + }); + + test("drawRRect calls Canvas's drawRRect", () { + CanvasWrapper() + ..canvas = canvas + ..drawRRect(rRect, paint); + verify( + () => canvas.drawRRect(rRect, paint), + ).called(1); + }); + + test("drawRawAtlas calls Canvas's drawRawAtlas", () { + CanvasWrapper() + ..canvas = canvas + ..drawRawAtlas( + atlas, + float32list, + float32list, + int32list, + BlendMode.clear, + rect, + paint, + ); + verify( + () => canvas.drawRawAtlas( + atlas, + float32list, + float32list, + int32list, + BlendMode.clear, + rect, + paint, + ), + ).called(1); + }); + + test("drawRawPoints calls Canvas's drawRawPoints", () { + CanvasWrapper() + ..canvas = canvas + ..drawRawPoints(PointMode.points, float32list, paint); + verify( + () => canvas.drawRawPoints(PointMode.points, float32list, paint), + ).called(1); + }); + + test("drawRect calls Canvas's drawRect", () { + CanvasWrapper() + ..canvas = canvas + ..drawRect(rect, paint); + verify( + () => canvas.drawRect(rect, paint), + ).called(1); + }); + + test("drawShadow calls Canvas's drawShadow", () { + CanvasWrapper() + ..canvas = canvas + ..drawShadow(path, color, 0, false); + verify( + () => canvas.drawShadow(path, color, 0, false), + ).called(1); + }); + + test("drawVertices calls Canvas's drawVertices", () { + CanvasWrapper() + ..canvas = canvas + ..drawVertices(vertices, blendMode, paint); + verify( + () => canvas.drawVertices(vertices, blendMode, paint), + ).called(1); + }); + + test("getSaveCount calls Canvas's getSaveCount", () { + final canvasWrapper = CanvasWrapper()..canvas = canvas; + when(() => canvas.getSaveCount()).thenReturn(1); + canvasWrapper.getSaveCount(); + verify(() => canvas.getSaveCount()).called(1); + expect(canvasWrapper.getSaveCount(), 1); + }); + + test("restore calls Canvas's restore", () { + CanvasWrapper() + ..canvas = canvas + ..restore(); + verify(() => canvas.restore()).called(1); + }); + + test("rotate calls Canvas's rotate", () { + CanvasWrapper() + ..canvas = canvas + ..rotate(0); + verify(() => canvas.rotate(0)).called(1); + }); + + test("save calls Canvas's save", () { + CanvasWrapper() + ..canvas = canvas + ..save(); + verify(() => canvas.save()).called(1); + }); + + test("saveLayer calls Canvas's saveLayer", () { + CanvasWrapper() + ..canvas = canvas + ..saveLayer(rect, paint); + verify(() => canvas.saveLayer(rect, paint)).called(1); + }); + + test("scale calls Canvas's scale", () { + CanvasWrapper() + ..canvas = canvas + ..scale(0, 0); + verify(() => canvas.scale(0, 0)).called(1); + }); + + test("skew calls Canvas's skew", () { + CanvasWrapper() + ..canvas = canvas + ..skew(0, 0); + verify(() => canvas.skew(0, 0)).called(1); + }); + + test("transform calls Canvas's transform", () { + CanvasWrapper() + ..canvas = canvas + ..transform(float64list); + verify(() => canvas.transform(float64list)).called(1); + }); + + test("translate calls Canvas's translate", () { + CanvasWrapper() + ..canvas = canvas + ..translate(0, 0); + verify(() => canvas.translate(0, 0)).called(1); + }); + }); + }); +} diff --git a/packages/pinball_flame/test/src/canvas/z_canvas_component_test.dart b/packages/pinball_flame/test/src/canvas/z_canvas_component_test.dart new file mode 100644 index 00000000..67c45ec7 --- /dev/null +++ b/packages/pinball_flame/test/src/canvas/z_canvas_component_test.dart @@ -0,0 +1,80 @@ +// 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/material.dart' hide Image; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestCircleComponent extends CircleComponent with ZIndex { + _TestCircleComponent(Color color) + : super( + paint: Paint()..color = color, + radius: 10, + ); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('ZCanvasComponent', () { + final flameTester = FlameTester(FlameGame.new); + const goldensFilePath = '../goldens/rendering/'; + + test('can be instantiated', () { + expect( + ZCanvasComponent(), + isA(), + ); + }); + + flameTester.test('loads correctly', (game) async { + final component = ZCanvasComponent(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + + flameTester.testGameWidget( + 'red circle renders behind blue circle', + setUp: (game, tester) async { + final canvas = ZCanvasComponent( + children: [ + _TestCircleComponent(Colors.blue)..zIndex = 1, + _TestCircleComponent(Colors.red)..zIndex = 0, + ], + ); + await game.ensureAdd(canvas); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('${goldensFilePath}red_blue.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'blue circle renders behind red circle', + setUp: (game, tester) async { + final canvas = ZCanvasComponent( + children: [ + _TestCircleComponent(Colors.blue)..zIndex = 0, + _TestCircleComponent(Colors.red)..zIndex = 1 + ], + ); + await game.ensureAdd(canvas); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('${goldensFilePath}blue_red.png'), + ); + }, + ); + }); +} diff --git a/packages/pinball_flame/test/src/goldens/rendering/blue_red.png b/packages/pinball_flame/test/src/goldens/rendering/blue_red.png new file mode 100644 index 00000000..4ca86375 Binary files /dev/null and b/packages/pinball_flame/test/src/goldens/rendering/blue_red.png differ diff --git a/packages/pinball_flame/test/src/goldens/rendering/red_blue.png b/packages/pinball_flame/test/src/goldens/rendering/red_blue.png new file mode 100644 index 00000000..a657024f Binary files /dev/null and b/packages/pinball_flame/test/src/goldens/rendering/red_blue.png differ diff --git a/packages/pinball_flame/test/src/rendering/z_canvas_component_test.dart b/packages/pinball_flame/test/src/rendering/z_canvas_component_test.dart deleted file mode 100644 index b6007bc5..00000000 --- a/packages/pinball_flame/test/src/rendering/z_canvas_component_test.dart +++ /dev/null @@ -1,385 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'dart:typed_data'; -import 'dart:ui'; - -import 'package:flame/components.dart'; -import 'package:flame/game.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter/material.dart' hide Image; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -class _TestCircleComponent extends CircleComponent with ZIndex { - _TestCircleComponent(Color color) - : super( - paint: Paint()..color = color, - radius: 10, - ); -} - -class _MockCanvas extends Mock implements Canvas {} - -class _MockImage extends Mock implements Image {} - -class _MockPicture extends Mock implements Picture {} - -class _MockParagraph extends Mock implements Paragraph {} - -class _MockVertices extends Mock implements Vertices {} - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(FlameGame.new); - const goldenPrefix = 'golden/rendering/'; - - group('ZCanvasComponent', () { - flameTester.test('loads correctly', (game) async { - final component = ZCanvasComponent(); - await game.ensureAdd(component); - expect(game.contains(component), isTrue); - }); - - flameTester.testGameWidget( - 'red circle renders behind blue circle', - setUp: (game, tester) async { - final canvas = ZCanvasComponent( - children: [ - _TestCircleComponent(Colors.blue)..zIndex = 1, - _TestCircleComponent(Colors.red)..zIndex = 0, - ], - ); - await game.ensureAdd(canvas); - - game.camera.followVector2(Vector2.zero()); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('${goldenPrefix}red_blue.png'), - ); - }, - ); - - flameTester.testGameWidget( - 'blue circle renders behind red circle', - setUp: (game, tester) async { - final canvas = ZCanvasComponent( - children: [ - _TestCircleComponent(Colors.blue)..zIndex = 0, - _TestCircleComponent(Colors.red)..zIndex = 1 - ], - ); - await game.ensureAdd(canvas); - - game.camera.followVector2(Vector2.zero()); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('${goldenPrefix}blue_red.png'), - ); - }, - ); - }); - - group('ZCanvas', () { - late Canvas canvas; - late Path path; - late RRect rRect; - late Rect rect; - late Paint paint; - late Image atlas; - late BlendMode blendMode; - late Color color; - late Offset offset; - late Float64List float64list; - late Float32List float32list; - late Int32List int32list; - late Picture picture; - late Paragraph paragraph; - late Vertices vertices; - - setUp(() { - canvas = _MockCanvas(); - path = Path(); - rRect = RRect.zero; - rect = Rect.zero; - paint = Paint(); - atlas = _MockImage(); - blendMode = BlendMode.clear; - color = Colors.black; - offset = Offset.zero; - float64list = Float64List(1); - float32list = Float32List(1); - int32list = Int32List(1); - picture = _MockPicture(); - paragraph = _MockParagraph(); - vertices = _MockVertices(); - }); - - test("clipPath calls Canvas's clipPath", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.clipPath(path, doAntiAlias: false); - verify( - () => canvas.clipPath(path, doAntiAlias: false), - ).called(1); - }); - - test("clipRRect calls Canvas's clipRRect", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.clipRRect(rRect, doAntiAlias: false); - verify( - () => canvas.clipRRect(rRect, doAntiAlias: false), - ).called(1); - }); - - test("clipRect calls Canvas's clipRect", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.clipRect(rect, doAntiAlias: false); - verify( - () => canvas.clipRect(rect, doAntiAlias: false), - ).called(1); - }); - - test("drawArc calls Canvas's drawArc", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawArc(rect, 0, 1, false, paint); - verify( - () => canvas.drawArc(rect, 0, 1, false, paint), - ).called(1); - }); - - test("drawAtlas calls Canvas's drawAtlas", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawAtlas(atlas, [], [], [], blendMode, rect, paint); - verify( - () => canvas.drawAtlas(atlas, [], [], [], blendMode, rect, paint), - ).called(1); - }); - - test("drawCircle calls Canvas's drawCircle", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawCircle(offset, 0, paint); - verify( - () => canvas.drawCircle(offset, 0, paint), - ).called(1); - }); - - test("drawColor calls Canvas's drawColor", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawColor(color, blendMode); - verify( - () => canvas.drawColor(color, blendMode), - ).called(1); - }); - - test("drawDRRect calls Canvas's drawDRRect", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawDRRect(rRect, rRect, paint); - verify( - () => canvas.drawDRRect(rRect, rRect, paint), - ).called(1); - }); - - test("drawImage calls Canvas's drawImage", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawImage(atlas, offset, paint); - verify( - () => canvas.drawImage(atlas, offset, paint), - ).called(1); - }); - - test("drawImageNine calls Canvas's drawImageNine", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawImageNine(atlas, rect, rect, paint); - verify( - () => canvas.drawImageNine(atlas, rect, rect, paint), - ).called(1); - }); - - test("drawImageRect calls Canvas's drawImageRect", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawImageRect(atlas, rect, rect, paint); - verify( - () => canvas.drawImageRect(atlas, rect, rect, paint), - ).called(1); - }); - - test("drawLine calls Canvas's drawLine", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawLine(offset, offset, paint); - verify( - () => canvas.drawLine(offset, offset, paint), - ).called(1); - }); - - test("drawOval calls Canvas's drawOval", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawOval(rect, paint); - verify( - () => canvas.drawOval(rect, paint), - ).called(1); - }); - - test("drawPaint calls Canvas's drawPaint", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawPaint(paint); - verify( - () => canvas.drawPaint(paint), - ).called(1); - }); - - test("drawParagraph calls Canvas's drawParagraph", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawParagraph(paragraph, offset); - verify( - () => canvas.drawParagraph(paragraph, offset), - ).called(1); - }); - - test("drawPath calls Canvas's drawPath", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawPath(path, paint); - verify( - () => canvas.drawPath(path, paint), - ).called(1); - }); - - test("drawPicture calls Canvas's drawPicture", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawPicture(picture); - verify( - () => canvas.drawPicture(picture), - ).called(1); - }); - - test("drawPoints calls Canvas's drawPoints", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawPoints(PointMode.points, [offset], paint); - verify( - () => canvas.drawPoints(PointMode.points, [offset], paint), - ).called(1); - }); - - test("drawRRect calls Canvas's drawRRect", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawRRect(rRect, paint); - verify( - () => canvas.drawRRect(rRect, paint), - ).called(1); - }); - - test("drawRawAtlas calls Canvas's drawRawAtlas", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawRawAtlas( - atlas, - float32list, - float32list, - int32list, - BlendMode.clear, - rect, - paint, - ); - verify( - () => canvas.drawRawAtlas( - atlas, - float32list, - float32list, - int32list, - BlendMode.clear, - rect, - paint, - ), - ).called(1); - }); - - test("drawRawPoints calls Canvas's drawRawPoints", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawRawPoints(PointMode.points, float32list, paint); - verify( - () => canvas.drawRawPoints(PointMode.points, float32list, paint), - ).called(1); - }); - - test("drawRect calls Canvas's drawRect", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawRect(rect, paint); - verify( - () => canvas.drawRect(rect, paint), - ).called(1); - }); - - test("drawShadow calls Canvas's drawShadow", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawShadow(path, color, 0, false); - verify( - () => canvas.drawShadow(path, color, 0, false), - ).called(1); - }); - - test("drawVertices calls Canvas's drawVertices", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawVertices(vertices, blendMode, paint); - verify( - () => canvas.drawVertices(vertices, blendMode, paint), - ).called(1); - }); - - test("getSaveCount calls Canvas's getSaveCount", () { - final zcanvas = ZCanvas()..canvas = canvas; - when(() => canvas.getSaveCount()).thenReturn(1); - zcanvas.getSaveCount(); - verify(() => canvas.getSaveCount()).called(1); - }); - - test("restore calls Canvas's restore", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.restore(); - verify(() => canvas.restore()).called(1); - }); - - test("rotate calls Canvas's rotate", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.rotate(0); - verify(() => canvas.rotate(0)).called(1); - }); - - test("save calls Canvas's save", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.save(); - verify(() => canvas.save()).called(1); - }); - - test("saveLayer calls Canvas's saveLayer", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.saveLayer(rect, paint); - verify(() => canvas.saveLayer(rect, paint)).called(1); - }); - - test("scale calls Canvas's scale", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.scale(0, 0); - verify(() => canvas.scale(0, 0)).called(1); - }); - - test("skew calls Canvas's skew", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.skew(0, 0); - verify(() => canvas.skew(0, 0)).called(1); - }); - - test("transform calls Canvas's transform", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.transform(float64list); - verify(() => canvas.transform(float64list)).called(1); - }); - - test("translate calls Canvas's translate", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.translate(0, 0); - verify(() => canvas.translate(0, 0)).called(1); - }); - }); -} diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index e1ed3084..f1f3a4cb 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -1,6 +1,9 @@ // ignore_for_file: cascade_invocations +import 'dart:ui'; + import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; import 'package:flame/input.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/gestures.dart'; @@ -233,14 +236,40 @@ void main() { }, ); - flameBlocTester.test( - 'one SkillShot', - (game) async { + flameBlocTester.test('one SkillShot', (game) async { + await game.ready(); + expect( + game.descendants().whereType().length, + equals(1), + ); + }); + + flameBlocTester.testGameWidget( + 'paints sprites with FilterQuality.medium', + setUp: (game, tester) async { + await game.images.loadAll(assets); await game.ready(); + + final descendants = game.descendants(); + final components = [ + ...descendants.whereType(), + ...descendants.whereType(), + ]; + expect(components, isNotEmpty); expect( - game.descendants().whereType().length, - equals(1), + components.whereType().length, + equals(components.length), ); + + await tester.pump(); + + for (final component in components) { + if (component is! HasPaint) return; + expect( + component.paint.filterQuality, + equals(FilterQuality.medium), + ); + } }, ); @@ -308,28 +337,25 @@ void main() { ); }); - group( - 'onNewState', - () { - flameTester.test( - 'spawns a ball', - (game) async { - final previousBalls = - game.descendants().whereType().toList(); - - game.controller.onNewState(_MockGameState()); - await game.ready(); - final currentBalls = - game.descendants().whereType().toList(); - - expect( - currentBalls.length, - equals(previousBalls.length + 1), - ); - }, - ); - }, - ); + group('onNewState', () { + flameTester.test( + 'spawns a ball', + (game) async { + final previousBalls = + game.descendants().whereType().toList(); + + game.controller.onNewState(_MockGameState()); + await game.ready(); + final currentBalls = + game.descendants().whereType().toList(); + + expect( + currentBalls.length, + equals(previousBalls.length + 1), + ); + }, + ); + }); }); });