From c65dfb60012247522e4d6165407aa7444b21c39b Mon Sep 17 00:00:00 2001 From: Erick Date: Tue, 5 Apr 2022 12:36:36 -0300 Subject: [PATCH 1/5] feat: adds sound effects (#143) * feat: adding placeholder sound effects * feat: adding sound effects * fix: lint * fix: adding pinball audio to mock * fix: tests --- .github/workflows/pinball_audio.yaml | 19 +++ lib/app/view/app.dart | 17 ++- lib/game/components/bonus_word.dart | 5 +- lib/game/components/score_points.dart | 2 + lib/game/pinball_game.dart | 13 +- lib/game/view/pinball_game_page.dart | 20 ++- lib/gen/assets.gen.dart | 3 - lib/main_development.dart | 7 +- lib/main_production.dart | 7 +- lib/main_staging.dart | 7 +- packages/pinball_audio/.gitignore | 39 ++++++ packages/pinball_audio/README.md | 11 ++ packages/pinball_audio/analysis_options.yaml | 4 + packages/pinball_audio/assets/sfx/google.ogg | Bin 0 -> 12240 bytes packages/pinball_audio/assets/sfx/plim.ogg | Bin 0 -> 10600 bytes .../pinball_audio/lib/gen/assets.gen.dart | 69 ++++++++++ packages/pinball_audio/lib/pinball_audio.dart | 3 + .../pinball_audio/lib/src/pinball_audio.dart | 71 ++++++++++ packages/pinball_audio/pubspec.yaml | 28 ++++ .../pinball_audio/test/helpers/helpers.dart | 1 + .../pinball_audio/test/helpers/mocks.dart | 34 +++++ .../test/src/pinball_audio_test.dart | 110 +++++++++++++++ .../lib/gen/assets.gen.dart | 46 ------- pubspec.lock | 128 +++++++++++++++++- pubspec.yaml | 2 + test/app/view/app_test.dart | 8 +- test/game/components/bonus_word_test.dart | 24 ++++ test/game/components/score_points_test.dart | 22 ++- test/helpers/builders.dart | 8 +- test/helpers/extensions.dart | 4 + test/helpers/mocks.dart | 3 + test/helpers/pump_app.dart | 55 +++++--- 32 files changed, 685 insertions(+), 85 deletions(-) create mode 100644 .github/workflows/pinball_audio.yaml create mode 100644 packages/pinball_audio/.gitignore create mode 100644 packages/pinball_audio/README.md create mode 100644 packages/pinball_audio/analysis_options.yaml create mode 100644 packages/pinball_audio/assets/sfx/google.ogg create mode 100644 packages/pinball_audio/assets/sfx/plim.ogg create mode 100644 packages/pinball_audio/lib/gen/assets.gen.dart create mode 100644 packages/pinball_audio/lib/pinball_audio.dart create mode 100644 packages/pinball_audio/lib/src/pinball_audio.dart create mode 100644 packages/pinball_audio/pubspec.yaml create mode 100644 packages/pinball_audio/test/helpers/helpers.dart create mode 100644 packages/pinball_audio/test/helpers/mocks.dart create mode 100644 packages/pinball_audio/test/src/pinball_audio_test.dart diff --git a/.github/workflows/pinball_audio.yaml b/.github/workflows/pinball_audio.yaml new file mode 100644 index 00000000..7a43413a --- /dev/null +++ b/.github/workflows/pinball_audio.yaml @@ -0,0 +1,19 @@ +name: pinball_audio + +on: + push: + paths: + - "packages/pinball_audio/**" + - ".github/workflows/pinball_audio.yaml" + + pull_request: + paths: + - "packages/pinball_audio/**" + - ".github/workflows/pinball_audio.yaml" + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + working_directory: packages/pinball_audio + coverage_excludes: "lib/gen/*.dart" diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 8de80730..521d575e 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -13,18 +13,27 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/landing/landing.dart'; +import 'package:pinball_audio/pinball_audio.dart'; class App extends StatelessWidget { - const App({Key? key, required LeaderboardRepository leaderboardRepository}) - : _leaderboardRepository = leaderboardRepository, + const App({ + Key? key, + required LeaderboardRepository leaderboardRepository, + required PinballAudio pinballAudio, + }) : _leaderboardRepository = leaderboardRepository, + _pinballAudio = pinballAudio, super(key: key); final LeaderboardRepository _leaderboardRepository; + final PinballAudio _pinballAudio; @override Widget build(BuildContext context) { - return RepositoryProvider.value( - value: _leaderboardRepository, + return MultiRepositoryProvider( + providers: [ + RepositoryProvider.value(value: _leaderboardRepository), + RepositoryProvider.value(value: _pinballAudio), + ], child: MaterialApp( title: 'I/O Pinball', theme: ThemeData( diff --git a/lib/game/components/bonus_word.dart b/lib/game/components/bonus_word.dart index e7f1626a..3457e84c 100644 --- a/lib/game/components/bonus_word.dart +++ b/lib/game/components/bonus_word.dart @@ -13,7 +13,8 @@ import 'package:pinball_components/pinball_components.dart'; /// {@template bonus_word} /// Loads all [BonusLetter]s to compose a [BonusWord]. /// {@endtemplate} -class BonusWord extends Component with BlocComponent { +class BonusWord extends Component + with BlocComponent, HasGameRef { /// {@macro bonus_word} BonusWord({required Vector2 position}) : _position = position; @@ -29,6 +30,8 @@ class BonusWord extends Component with BlocComponent { @override void onNewState(GameState state) { if (state.bonusHistory.last == GameBonus.word) { + gameRef.audio.googleBonus(); + final letters = children.whereType().toList(); for (var i = 0; i < letters.length; i++) { diff --git a/lib/game/components/score_points.dart b/lib/game/components/score_points.dart index 39910777..ce13c718 100644 --- a/lib/game/components/score_points.dart +++ b/lib/game/components/score_points.dart @@ -37,5 +37,7 @@ class BallScorePointsCallback extends ContactCallback { _gameRef.read().add( Scored(points: scorePoints.points), ); + + _gameRef.audio.score(); } } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 6f4a0c81..e09ab461 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -8,17 +8,20 @@ import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/gen/assets.gen.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart' hide Assets; class PinballGame extends Forge2DGame with FlameBloc, HasKeyboardHandlerComponents { - PinballGame({required this.theme}) { + PinballGame({required this.theme, required this.audio}) { images.prefix = ''; } final PinballTheme theme; + final PinballAudio audio; + late final Plunger plunger; @override @@ -109,7 +112,13 @@ class PinballGame extends Forge2DGame } class DebugPinballGame extends PinballGame with TapDetector { - DebugPinballGame({required PinballTheme theme}) : super(theme: theme); + DebugPinballGame({ + required PinballTheme theme, + required PinballAudio audio, + }) : super( + theme: theme, + audio: audio, + ); @override Future onLoad() async { diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 0fa6a1ad..e50eb2d7 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_theme/pinball_theme.dart'; class PinballGamePage extends StatelessWidget { @@ -51,13 +52,24 @@ class _PinballGameViewState extends State { void initState() { super.initState(); + final audio = context.read(); + + _game = widget._isDebugMode + ? DebugPinballGame(theme: widget.theme, audio: audio) + : PinballGame(theme: widget.theme, audio: audio); + // TODO(erickzanardo): Revisit this when we start to have more assets // this could expose a Stream (maybe even a cubit?) so we could show the // the loading progress with some fancy widgets. - _game = (widget._isDebugMode - ? DebugPinballGame(theme: widget.theme) - : PinballGame(theme: widget.theme)) - ..preLoadAssets(); + _fetchAssets(); + } + + Future _fetchAssets() async { + final pinballAudio = context.read(); + await Future.wait([ + _game.preLoadAssets(), + pinballAudio.load(), + ]); } @override diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 97be7f3e..370d8fcf 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -3,8 +3,6 @@ /// FlutterGen /// ***************************************************** -// ignore_for_file: directives_ordering,unnecessary_import - import 'package:flutter/widgets.dart'; class $AssetsImagesGen { @@ -17,7 +15,6 @@ class $AssetsImagesGen { class $AssetsImagesComponentsGen { const $AssetsImagesComponentsGen(); - /// File path: assets/images/components/background.png AssetGenImage get background => const AssetGenImage('assets/images/components/background.png'); } diff --git a/lib/main_development.dart b/lib/main_development.dart index 8673eff4..8944073d 100644 --- a/lib/main_development.dart +++ b/lib/main_development.dart @@ -8,10 +8,15 @@ import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/app/app.dart'; import 'package:pinball/bootstrap.dart'; +import 'package:pinball_audio/pinball_audio.dart'; void main() { bootstrap((firestore) async { final leaderboardRepository = LeaderboardRepository(firestore); - return App(leaderboardRepository: leaderboardRepository); + final pinballAudio = PinballAudio(); + return App( + leaderboardRepository: leaderboardRepository, + pinballAudio: pinballAudio, + ); }); } diff --git a/lib/main_production.dart b/lib/main_production.dart index 8673eff4..8944073d 100644 --- a/lib/main_production.dart +++ b/lib/main_production.dart @@ -8,10 +8,15 @@ import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/app/app.dart'; import 'package:pinball/bootstrap.dart'; +import 'package:pinball_audio/pinball_audio.dart'; void main() { bootstrap((firestore) async { final leaderboardRepository = LeaderboardRepository(firestore); - return App(leaderboardRepository: leaderboardRepository); + final pinballAudio = PinballAudio(); + return App( + leaderboardRepository: leaderboardRepository, + pinballAudio: pinballAudio, + ); }); } diff --git a/lib/main_staging.dart b/lib/main_staging.dart index 8673eff4..8944073d 100644 --- a/lib/main_staging.dart +++ b/lib/main_staging.dart @@ -8,10 +8,15 @@ import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/app/app.dart'; import 'package:pinball/bootstrap.dart'; +import 'package:pinball_audio/pinball_audio.dart'; void main() { bootstrap((firestore) async { final leaderboardRepository = LeaderboardRepository(firestore); - return App(leaderboardRepository: leaderboardRepository); + final pinballAudio = PinballAudio(); + return App( + leaderboardRepository: leaderboardRepository, + pinballAudio: pinballAudio, + ); }); } diff --git a/packages/pinball_audio/.gitignore b/packages/pinball_audio/.gitignore new file mode 100644 index 00000000..d6130351 --- /dev/null +++ b/packages/pinball_audio/.gitignore @@ -0,0 +1,39 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VSCode related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/packages/pinball_audio/README.md b/packages/pinball_audio/README.md new file mode 100644 index 00000000..f8b69df7 --- /dev/null +++ b/packages/pinball_audio/README.md @@ -0,0 +1,11 @@ +# pinball_audio + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +Package with the sound manager for the pinball game + +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis \ No newline at end of file diff --git a/packages/pinball_audio/analysis_options.yaml b/packages/pinball_audio/analysis_options.yaml new file mode 100644 index 00000000..f8155aa6 --- /dev/null +++ b/packages/pinball_audio/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:very_good_analysis/analysis_options.2.4.0.yaml +analyzer: + exclude: + - lib/**/*.gen.dart diff --git a/packages/pinball_audio/assets/sfx/google.ogg b/packages/pinball_audio/assets/sfx/google.ogg new file mode 100644 index 0000000000000000000000000000000000000000..dafaa8d41c29878463f742fd80d4cf5645ab065d GIT binary patch literal 12240 zcmaiZ1zc1?*Y{nzMY@DVL0C#^fdy%h7FfEayHi>TX^`#?N$Cbbq`Q_95Tztl1Qb-h z3qJKd-}igp`34_Le*H$SZq)$5i2D%UHoDLYxYC&V&W|5q7C|I3IUr0d$c*l?-3SwkIc&9raW zLlvOB+`Qb}g53O2R-~ncr@5_@HB{Eg%ht`=$s~*1WO{OBKb3h5&%i$$D8B_c0AK*YthjMS z+X{9Q2x}^qIJbO+?PH-bD0YSX8C_avn0n$;#v3m2dsi3D<5100|ztepk9-&>Be2SvR19_C{)E zv9y(^XWe+vVS@S-=u6m0Mt^?BRHplc0zqcU-`-P_EbQv?8E}kxC!1)1+7z5;JCliB zzN8DT)tk!Pe$_?>_9BzBSen{2|ML3>EXs|uCwdVZ;EKrQEc-TGBe3_=BKv3@_j>HV z;UfY1&2*7!Am=$WAP&onjIoq4iQXfgC@G(32v$rV8}~@DJVXsi%~Im={6=#q0E80X zp!lE74V3?)xHvJ2ZG^R9jCYjdx~b}$5Zf`w4Li-gOIl`X+r6< zmn0(jY&Qe%v2pG-6EoZeX=e;bKnk@}l z0`!{y5iozlW_`i)KO*NkMD8NuKFKFy{5x{;S>rwm#obd#WYSB#|1{M)GP67*_j7q6 z-hV}oO;lNWR9QsSPDC7QWU6gsW<^_}!${43`~O=16*+QlOkf2gN7jw$KO(1xhfW%d zrg{#QlfOEOPJ;&Zkfr=j2LM1%0)f)aIiil_oJYdvk(^qZqW^QmfYf<@m05n!uqgmQ z4FLB*Cx;S8xg|(BiR$9;Y%^gNAg~JFQH7Qf^AR8j?|LLivWT`OBKinUJ|J1iWt2l= znFZsZgl|g?6}E(Mz+(>sPzQiuo*|xhRBjUr!}&53Jh(Z@LkO0Vq!|SmXDSsRjJcG= z{8M((jKXknGL=$PPBDpjWHq=5zy|;D3#JmtArfQ&U`Bw%gM}p{Ct%wN$Pu0`I;0|^ zEL&p&fpaHxga<1pgcw#rr!fH=qzfIB+{zA_l4Jz{Xb%MZlVat!kOhcYfDbNAlS%wa zWXwvWvXf+%3#2LwDg5L7aCJ?sQBtjWO|2CQ6|H7w6;c(I@0wbqvRdR?v#BatO^%cN zmP?vis}x%E&6X<^fjX(4>wGGD;3Ep8dM=~}n4lbu(*MCqh3tlvfYw4YT-`%UUCUBW zQ%ldwQtw<#2Nj?MQZ-bT$hEYZwU$mDwNe8%JhY5-ESJu;^e6%i7Q!~p0ye0wIWNq6 zf>cwKrD==jI;(A_tzF@(icHpuit>tP$BLS2gTmd4!itf4>x!DHk^0w_H4ghAwdPet zJx4_aM`eB0Wd+B6^Q(&X>XG`|teVRG=41Rmj{SC*ikh0r`h#m~d-eW+>wY`8e<^IV zv~sV#yuICNzuk4L*^#cOVy~NR^lj}({qcVLgGNtAbuG`;WTQ_ypdOAU-njldxKn62E5R=YyZQ!y7Ct7f9;DlY=R4A^*IuqP!MRit1<$k>rdtcSQl)6@cQHtfd zpphP>)4E`s!9r|d#a=txXg4T`=ly^)T?aRv(TV^_$f>W!4=^7($DOoa8F0N6{&eP9 z^qK&7FdoDP}bJ45CDsDhfIT*tsm+^jNF43Yw}<*x<6Lzr%P5-v|~)3 zV;g{K#wiSCCy^-Hz>;T>*aanOiuMJ`D5QN|>73aiw3tHCmNR)q->!UMPCw?hIWg}} zvBiV}Zb{=c0c5q4UrZ#WSW--;q{yi~1_HTgMpC}Km_$RNya{AgQcR(w$kR+_;KYNX z16LH27%0F7$TS=|2Qu#sKtWc+*TQdPY^5iItTI9p6&0&f5|LxsN!P4G;uIBN&54T2 z;I0%E>%x%w4m_xAqyrq>704=4(SdV7-$1#^lUN`BW>?is+V*zU1E6r`5-KIdx+V&H zC!TE@Mce9Y;pP1l263Qp4SRSg%I=zA;3$g9bXTlK%^_n!g1KYYe5T*=KQ%YA;~YSl zRbLBdt4m1)g>NtG#~_kt%s^I&$QW>ifkRtqsvStkL`FAlFS|Ry&*t>2u4O2JZwUAJ zwo`5J)CoohB!Y1*^RJJP>#&uKKu##w61$Bd*l?xB5G>_x%2F_x)|ey?bJGL@2J1&5 zaF|Odl-M}?we4XrPzz2l@3;wDES1Q92p*vxE+7L&@c# zdT8>p-40b&WGR0JTC~~|ghEOGn7&<}U;9CC0Q;Ja!J0XD=^AOXzx3PQl9BzfkV zfK>bp1;=0|9@m6qBpv{;xqul_t-6K#^OyPHnTZBGy*@J)5!eOEk6;7PMGV4fKwnz1x?k76hOq=?Rs~-GLEN;RX|D(p?8n;x;5{Zb zP>W70E5Jg9%f@eUqeL(iG-C`;IGI@#&rY^94y->N%yCMou2T>2C3~HJ#DE0waR4MB zcId8~Z1=c-Cno>y;r@S=CT7ZPl2=|f|VIW z2@D3p2aM=seZ>rSB$AD}B=!2}nA2C}1bJgKm!Ob#pcG-8lE%4fQHzrDuo9GhG$$Bx zir}MJ3_KhOPU&AXtKuo1L4xr~WnKjakiI>*b$d=&6Ur>63A8_WLZHl|;V6B4;DeJ3 z5G-Ho8lF>Bi}O#2pQE7C@2&I2ISZFOz{|EU~H1`;t}8thPuz#q65UCLj*4) zbO}gEE1nga2?M%@Plm9A31VX#qw#lndoYdOdqP8@7>p795VC~e5KoAR}oZiR7o2FO1PFaL*+D~qdhlkdhC z=jPtec1;fTy&eA7&HUHb{3J!z2DG-}3pPeh;h(1-iBr<`^0(G_`*g)79YxZzlEn4c zV$)kIc0FcQilkt;{8&%F<;M7KWg1-}W3|*FsXWn2^59s8v!T;c((V)n#P$#L}z#0&BiK zDLBa_s%;pyqyD!c%&Xm1s7Mr7kA(&>D^FY&I>mQpBVb6V1Sz1k@@DQWSIK3gwzQb6sS z8=2mTSG2jp1QxeJbl6Dnus`xJl;+Ggg>>G53R~>y=?2;B`D2o)zM7;F;6-cLVd^p1y*HY?fyvTuOgqvmJ<{HY0VA>BZ8PdD*NO)lCGBfQ~A3WLu=K8htCiUHnM58UnASa0s6L6 zlrWs_dbi>dC0{5oEb%+*4zA}HuHQZUB9wsUa{Uw{PBbedP4CoDySe{Y?UXF`x zR5)1Y%V)aHWfXLbCC&NFY4V@XZ0%`}gM=T9%H*qaVnj#kut+SQ6WLw9cG=2gd%RfH z{@4h61Bg12+H*O@@#rSU1oDT3es#jgO%9aN0C}I9-LavfdgbvP<+qOs2{${BYIJz5 zDkmQvp+9MW`*+$Z&L|n(d1q3`_M%3**(%&S8h01BCx*!3;uY+xNt?4#jkX8C)smi3 z`dsWc_jD>UI;)`an<2#Al86!5$@BQR(TGx{9s1b)@M*LW(JMCj*|D$@V z*y79MwL;0~WfBVV-rYKsHt<#5(Ug;%-~e|9mKTh)z#k$0aU;q+kqpL64oo0BIKRJc zWN*OQC1|Tkg>0NReN&QvaR)o^&x!+I#4 zPvbXi0P`Lt3obgaf0~as6wO&$nEzSZTb9*?4sS3(bPZ0cR1av{=d{*cc)zjewLhXN z5zX>HYGezDClLFX&$TebW!~UL%L+uFz0e-LtGgSV)zaTB>LL0$;v%4{jHg5}nBQ@W z&#v7t1f%3VHm-QpPu+^{Y${f+pR6qSVQaohcs-rc^`7rp#765UWAJ<}>G}&S^w@*B zUwgAC^X6b7a&^{pR7uqkbd2#+`n8Rt`H2?N50(NQ*Z|`Q&R!nUD ziW8BmX+m8tje0p(y72b(|l zyv_{2+LSjAQ`20vOh;*c>gI}mDQUe8V<6tr{WP5CSIoz@0i0g3n>+7vh}I{i*z9}U z3MHL=olU1CD*gR19tziBS@*qVpUMx7+8YRwj}EsD?(cj;&YO#(PcaxCol&7%=EWqx z^3GzG!qA0En4X}QaP&-j%U2V6OSvk2&Fjt1WTSOFMK`vAZ@{NBDc&``s!2i<2Y@lG zr2M_pIt^_eAhOc=0amSfiP&WOSq~lTczJNV0AF%%yK0)g$d1TsnO7wqC-~?iwS}vV zt@1ks=63t~pVOwHzIo52$&WLZ##n4r;*O2{ZNHVA1rwo*dYJq!%q2=5!oz~HZyjd8 z?u(&z+EP5&=MTiq(ULbURN5;c`)XP9(9w!qM|UawN@% zOAjN{G7hs)*ieTd(_a;QVB;kn0<-6TIW8tAt3YNDCs-7Y)ds&u3G^MK>LukW}1M;NAa& z9ewWFXr(b#p3dgiSpVEKI`6eqb&hZXN+{Vt_nv8>Q=~i5SIF+`^urpV7ODkzdSqzY zIHTF4xK1za1G0S0F`YW|!TNEUwFr1`CI_c?{jBceG~qQ3Fjgcsxb+~3jqyWFJTk?A z?y;y9H_k}Vmfq=6?}<2!-z;&>Kj5p^efukZ{2?2{SW)+tJkR}ty$@07-FjrMxeZU4 z{_IP*>=_)(u#YO6#>AO!|K8|u%TBou|NRDKgyC_vH{7Ps)av&xCQN(!#hLKCF6M() zz5D?g)u(H^Ut`;CND6}o6yq8FGTpNoF0HSG-0}z`zCjf{OFl}$YBf$SdOKh6_YtQ& z^-X@x-ViP4X#T9Iain+aKIDFxdZ4oES!*5>Byb7dtFLh<-g=gE#n#Z;1M?&H4mtDd ztH~}%Ki0kCS%nrz>}K{L0;r{~q|l((;Un`~W1Sj`5>MRjK)>pWwnbTy^HBPx&(zPs zw(f87U*;2iiSz;Pp1O}5Mt5P$@Xfx&lDVY)wi53S0Z=hjVTN5+A{h|i+3B0BiIH?z z65S?Vkr>PGlLK*p4{yo0%paRQCm&^Ty#5scbJQ;zK}HcT4S)BbJz9wk`g!T0twbxTui*`<@qzE6WUW9GsZ#*m-Ya zE>!#T>z1CFwZC(-;l<-d$EmjwpH$+Lf8(Y6_?Nmm#W(SxKTg9rTu_+GY6jd!8 z>s*SfvKIjzWv-PRHp3CgVo%*&0BF85o+zRvsYIZMI>;W*35R6R(gNn6)YcE-QfBBr zTc>{*OCKw}T$<>#nEcu2+E6&&MaDj(Ip#atdA#@I+XG#dUk+F?OgN5I-J-{o*0|Vs z(P)5vD)Y#pK?=PcE=lWfh@q;D1;(aT{Fuwb9JS1I2|J*>?R3d9<7el{*PpFjPEx-C za)yhXSgsWB?O?71h!F7ON!TFC69_R5^(X-l$w6fEE7ICezyE21(L;}&CP_+xndKRQ z;>uQ*i8XAPtCn!}oSRp<<7E?X!z&5(h|)E;1_H*Y=W}c@=STG{qdp?x>~)q+1qF$F z$;v9+IF~OLj)`DKo5E7$Nj z^T|%-toGSBgTc=ob!|m6I!azpOS6gyfcS##cXL;eU zGwLf{eYA+%E)|x|rh>bdb+3P1ntaTH24uA!@02>kt4}>f3+p+66zW+;*gzDOeZa@Q|k`n2yn8>aInqgEO|)&1Cwce$3hZ<<*xs6A?Aba-@Xtjl{l zs99{bfCbn1vS5XQZ!ewwnB4G@-KRbk_UH@14YsZcjoqu>UM~Eh;_BsNoacTgZJ$9>yR# zXx8otalAD{&0lN))RR~R^pOEme2eY$cd87}1WZso#~01t_+L9JSdkX*=?CvwV6SeP zJR;$08Fg*sbo&seK3;{2T&X%>muEkq*_xa&f37U=FJ|ezT0<&-7vgw@zv-5l%Bn=+ zj^3KP{zeKdSc-8NTQAuzvQ3=o^#@g=Suf=^I6Dt1Fpq|JMqcL`U(E4HyBb2u`B*w) z*7%;n?`$lUq!?vkfT^vu0(YjhjB|2#Ku`A2fwd6;j7N;-nlu}*?3hQ<|}8sylbGu&TBuIAf$<*RZ#3C1R>&FuYj* zwVnSQ|706lZnr2R*S=)hKb22N(usa5X4`Z8j1bC)u%6e*N7%qs^~MPezCJruv-fpZ z;2lX!bv|*Jhw{&QHj@yVC@XK;u=RxG0Q8puw?!@XdC^XS{2pa}n6{0H4-)1{mk_Kb z&yVM3Mk52D=2;U9{CU5z1@oX@9*EBM4!>V#@I0q#Y?5PD-c%XXoyrUc67h>_FD#-o zIcjN1QKNh6R<~c>4>@dL0R6~ngMB84L*DNN>pHRX5Nn#8JOMN!`4y*wq$h_j008{IBI>cd zNyi;aw!-r-Ia&5{kN4+(DvSS2Jk`1b`E7r=*QKum6+LAtF622(p8Q^(_3|C;QEXZA z%=m5^g+*o+obI-R75bBo5jp98h-xW0gq=`?tn~nfGk9U!yT8+U)Cn4-SSj0<=pNR9}SicrC-#Xo>G?Rk! zt?%;T)BzA$!lVjcWa?^2VZ@H?CrRb-3h1#1#kV^=8S zhq&m9Lu}ZRs{HeuFAR*(wk3Nd~zUj8g8skDVqM>ylI95;e;S;nI4t@h~S(`OC+)E!z3XT3? zOQq$*0!8{44m!iNx}AZ~IK$3Fo-=ky>!(C#zU{$c0gN+pc>OxU2M(hIQ*y~F80feb zf{q9d(Ez^SSxB(JKGuh9ia5xk3@wb-_l?Zc^H)oQA%fzZGgk_$)S4?;C9>*Q2m(7@ zWeeO*d7T)ka;^Y>z5u+69xdd3K6pWF%z(Lp*ov+I@<{mJ!o&H3Ccc9=ud5Icz`MB9 zYA}cxc%j=svSQ}?Q5cvai&?doD9l}eO%Xgvc2bVYJ8WHkcV2V&YJTm|)YZ43 z`?0~j5}F02E&Px7c7&DEX0}auHo739&4#Y}pa+c=?(nUA#z_O`NH{;$!N+QjD19 z=hIz)TrBXSjyfWknUGW;qaKepC+tX zl{s^&PZ)Oj<9$>q0IcB}{cP}8{#341ylDF2-krC$7d*iM&wwGI(Xj)tYZQ>O$Ahv{ z84CC5KKbQ^oJ?YV>JZ1>@NgqEMiROd%F^+&CsUo|`4c>Hi6V=5np704K&(*0?A8(4Es@*w?3qn=5NS8lgZtHACA(Jy(`^ z&jD%uf#_{%WxmIT2yJv{roS1yON#yN@A)L1~ea= z?~c9~C}$U#FtVQ6uazw5OfEoU^1XDS-!F3Gy!8m3x|g?zJ7PU}(Wl<+W6-kab5~=U zU?(3>MhkyZn~K)X)>M&i0*}U>E=v(~tneKR`uV75beew5J1*O1P32EIzxRHfSW6la z>z0WRE9j6BcnhUQj|JAKa^5nlW(s}VO#1PI12e#?EVu&~_=Hyr*tkc9SxG~Lm;7UQ zH|EFow}lFQ8xCAX6I^TPB|m$rN@RAlCV%{O7KwT<>Fi2Zka?j-(k>z|7x=y1Z188bo}P#cMYEe9%W49D!(BH~4W2s2CuezY3U6UAXz$LF zV7!iwsVW(?pb*u!OzR6R<)I(pw#^h^Ypsh^o(&HBC@GR2qqS5c76N-+#(IC}_5I%R z-_EOHtM{vc+~J3j_ne&;+lM75oqdPU^BxQHU}q)>&`LNI2}s>8L4W@zv~enBy{nh| zUagA7i0%r*jHFj7mspgY*J8`57Nbw>ZOT-J0f_YLow*l;(;BiEoG#qhF|jg@czozG_-4 zA?qg($<-+*{Ejaf_dL*H0g7isw1nANHJG|%|50&agYMkq87Akn+x)|Gf9Dc|3qn}j z8!mX{@c3Ib{O+gk6P14AML1>@r?!@9nd96c+&$IGv~$eB#fLOqjF`~+_&6Ec2?{z8 zSu~iUg7i*ehaaH?{ghL`$SEAJ1UO7Sn=R$Xd}=sMjN2A0xlrr8E@Jyx;LzV~$a?!E z+cQsEhY-7MYSw>+FRRs|O=rVIx7kQ?U~dgu=ezHr#~eSUSx8liyh!h7;XK{%SCsp# zW;kgOS-l@KQ@mOEAw5;udfz$Cq%%?q*p0q-X_5PO~`qN5fEtmFv4i+t#7%4tlq{Q9!eZv%-!nzsR4mhGqkr-WY1Uger!?R5`Frz zgSQLj(tYZh@G>Va&zWTKt;d~vmIE`<3J+eXK5*s4+@@Y_L4D=b=NOGylIwlkdntW* zVlf4c#-H$ecBuJ#sdWFVd}M_G+^bYW*V|U_n&gBmz46LXa~^0}gY_?3D>99t>^MeC zYZ;+%^_Kb?7oQeu&xBhm9SppGw#eNuT?fl%|5)?oItmQyZn1u3!ij^^zlDb4+7&>->T5e5>UtLXZOOA5U zx1I))yzmO>Be65=&pN!kXB@M5cSz}y2tD%jV-G@z7kh&_x4)i2anw&!={-RezO9g# z6@kR0W-*b={8Nt?46_9H%LNjBthx>hs>lzBlfkl17)k+_B6K(DMqzt2J zh+dUqU}HP(Vw5$B;s$w7ycsaPBg*?pa6dzRkI&kCNFI46-#R%ye@gJEI) zFe{mSE-p8<7;a4LaXEo8fO$V32-NFrYkT@cGa>bKtid$BI>cpXG*HV!-zsW(g{u2k z(iti*d+SKHrZ83Y^XHXYG}e)G2?lAdZn)$@V&3>qp7PE28qd)g?~h)_rln9G>P0)V z(wwFcB8QcCy31*u`e(M$@Gw6tzi1K3ApM@QM6(P{AmziOTq!W5?@YZXoQ5P}FQLHP z6#wbcS8AI(rdvh0?#7v}t>V7=d(Ho+ODG~|7xsZieQh2=k?J7$?We#`Q?ZQM(XBC! z-nX6e?O)*=L%(MBRe1E|Z+&jNE0q^@y5ARSlZ6HOBX77gd;U$@>CfrotV-1frB}+!#ywVsdM9=}dxy)#+&pYI`Yd83i9uW2=vp`TAUG+EtWa_tyOLd^_(~ zm@=mMh-V}lbJ3njPVojmLb*IUuDW6sM5}{TMUyF-c|-PtL>33o26I0mn$ZasM$0J8 zdJ@%O{_D3q(TzT{-u~(lr;oz~4J`g@Luip5ah_K5Vm zuqPMM{j;HYW2^kenRh;~a#lVIZBV{ybHezX*1s-Y-r~s>W8<7T*^aXx`{AA2ov(0A zt*z19Phx+D4C~`sX*anTT@kUz-R z*s`puViI90W4F!_-e?&8{2*}6qw$lax35DK>D*)bR(`1jBr$DJVtkDvm&l!JS_gSm z*_I008Ob`nxN}A>^N-c}Kc+Ds>DF%V^G)Lyszu8sq zRjHfkMA5&TNm$j`^GT}ZC*Bb$^#XUIyZrI)vzD^`h8cV{5=j@sO1Np16j1#09XSV5 zdM5b@rG%ECez3|UKwsg&a`3j5j&EM9xZTfVA!KAsywc!qn{7di8mG<|pz3$R0CzHW z%Ux=zZ?7}*IlTB1 zzPP<=)^_OraXS$<#WHdkhL))3%D~E$ZuuplWg*FG8PU>W-0dM6B=@m)y<1Jk@nT3( zrHHY-78A|n-L#6`>_#VUNVJAtts9R;;)JS3L|kdpd`Lcvk;sQHu5=$lnI|^VIPh45 zPYA{2IXv?!q@CR2KX@E-I*O=C=vMe6suNJ*u)Y10>TmmhC{+g-o_b%RCym=0I|L%j zy%*YkW|(Fz)TYqClUxaua8O1B)K7)?@tOy=TQ+rudoMV{sEKpp2~{=43#GFa9{(vg zg_E<}K8Y_%iNR&dz^OiMlc0KNG?~zOCe1HI=@@xdCGeo(x3%W;#+S7w%eSzJ<`tB% zUrCmARy-ut7O;g}Jo!~SxRdc|^=^M!|HoDCj@w=@N((+CIlJGcG?*}3&`OVTk&JXt z&Gx`sr}Z=0h932XVHxYh*ygpfhdrBo(mg#PARLIMwGq|c$U~25wUif>BXsA=hu!gP zzEQ9Kv_A)t<=>=~wwdvDGb)(FsBfMft+J9_Uu*)e`k_2rld}9OT1%VlQ#%LT9A=c* z1l{X>;l}yiT}9#5vL~khPfrN<-Q#II=|-A7FWcbNTTBN9|(ezstqS3(8x{WJfC5^Z%=K;r(GG4ALLkI@{b* zbG3#!*qZBHvWF?b1O)^I1jGb{VccjdOAiZMCu^9Tlc%lgV<$%|CwKUT5FCO(Jm50- zb#MS;a0HfHA*{hruzAVo+R2)i>`Kb>EWyts{ zhfyksF&&GNMnWSWw|?Y$^4|@~r5-SFFEWJmmQ*st7wkjex$&+bt3PVN0Dn+TKp_gE z^d8dlp0RI;Lv4>&gGOXXNKR8rRU0fGM!KF>6P_Lup1ykVK?Wau^*#g{Oa>Vp1(`vE z|E|Zr3r83AQgs*(kj{>bTZU({F=T#0X5wLji*NzU3zNV|qS@flSxUK&>`H8ts%;9Z z9ec`od&e@RD?Cip16Q zd`QwrOQ=>7S8aZL(xnF-`stTJU&6Z++R_sS6W#ii$P;t^^t`B(h>G)v!7=*vWQulr zGjN{ma3WFu)I)Hs-eBU|yIKg?3&kdJRn=4VV(h=+BL(`+Y!cd@5&-j!AT)=t=CZ~xyT74`Nk??TvzcE<$fNQD)zS1^Iqm_M zn!^A9Lv@Mbe>Im-{)6J|=rEpc?uuT)9^Q+lsI~vjR#_|6eR2edVqOst#jmT@Qr$|B ziuO5GeT)PWz28q}O% z9TMcz6cX3g($@8K)O+hOQxjw~>9H{3yD;(EQ9Z`&3Q5?$U9IC^-eS5-c`@%ysavH*_nzTkkv>qdLR+^$lBO+EK+E!zp zR#Vkh1~tBV)qe-fpRie&@c2jMT!aW4GGbXV8vj3$lg=IS<#q(SQZ$EN^bM~#>(Ipf zgw)OaOw#{~9GkGb_^`Z?u=S7#?$9{f(8Pk;Oo#50t-AlU{yTCMTsgoBMvj~-$3G&c z8NqlTjHYs4)%`y@iVuMXb(g#LPX_=%b0oR)6W?L>0r#0)FJ`Qo5L zaL!z6i{<33VWqBYEL1rxC7aqJv>03j5P^SW8H16uI8qP*FegWo!h^8re)w7>x*M^| zh(;mvk~RC0#7vlO1Yt@b6+DMgvmf5Ui0PGHO%5EC<^}**GYb~UFejjHNTEo~Y(ZCdS-I92T` z#{prhDJ|_eI_+_md!kxI!(CfL z+e%MMThG%DZrem#yUce5iQEMR`32RE1trA>nHvR}1>NP=1tmq@<)wus4qG6# zJD?L~>v_0mqM$5XO9*!nYQVouR z${p{1EdJFEG8jHa4m@V81(#JgT?fb7_isv?-HynyP6T&aP@GgyxK-|Sv$beTMnl`z zDAsC0%t-H=(}Gxp!Nlv#g0FQvJx!n>#BBRx#(DupqZv_6Xf1y@~688wVg{U598=@wCC*snB0x*;)ufR<{SJHb$&E z%8nH~%F_W5#o zpb}iqw(M6T$*H^`fUMTjvngayIoS|p6rWBn2;{6`X~q0(YE7m5Dv(u9Hk~pGQ4KM0 zLX0zlE3&B#l;G_UO-H`=MD}(V$g1l?_@#{1cr3^&Aut++Dvpgt_a?_&unLSoDZ#6w zQ7YiBP^kAoXnhC7crw~S1l$$KDjMa$*RF4%QsqIVPjJ#(=Udz7U?@MrRNK3fHt3$sM=5AQ(7`k0-jJipNLMuR(%^W8+xjv)zAbZf?id zK5kxoA)M!ZY&0l*ZCXDZi5)fvSw*A6!4(D$wYhP2AR!U`s%mZ8%|YaFRKMs#hBEku zaC=jGpaY&d{&;|tKZ#ZP#SwB5w$dT!ekEHf*IpzKiA*n&E8kT`1`gNml_ut_>PN!i zZR1E{&Kx>r9=gQ!qSoH<~oOUel5a}I;phm7Ec{+uJGAkS5d3W_WL1!&P?4-g7DZN2(-#g&s%#Kl45g36pA6do0Sj@Q2l0Ndb(wlB{S zP9Ty9kb-7`a*@_l=Do-bMq%(|WW-SLv?m8r$Pm+l+W_BHy-2X3k^u=|zLyaQHaXbg z3j!Lo2?NLAIqnw(ESeMmc$~qEs7%AsEg&i#JTq|tuZuGig(S+r%EQ}X*l08;4pb@M zl^nEPo6ZFRtG^8cq2VCnNd>~HUH?9+xb2~R32V-{d6CEj2;!=>M0*rSux{J^1(dost)+JZiikc{*#rIQ*TY$Nzc!N6!AA?EQc0 z8Q2$tko#u?NPQ$F0?vu9>nPlYGm!CK=txBYCVH*$;EoBIFv0MRYz*k0jQc1gHz$Y^ zI2?oz7|~dLRDv5C&BK`!cX4!#>ZABT-b9=^<7hij${pgI%Fzo^lhTUtoN@hEd|=3- zz(@0Nkzh19rESu@2$4OE2ICWIQ3M8%zCE~gdp>y8xOsRLXn*j87&m_I0GDmGl3xEzU*;=J(52O+$9(>)L<%y1v&p2!%~AXF4H?K`T~)l zX)XwtP`&JtuwIz!ax&|mwHLV{2vNL?Q;oOjPtR*J< z^9nxaCHbNuCjN6(!N2VPxhYF2{UKl6lrO~e?w`xh-wy zsd`WM*29QB$Y*AvfOgp}^^?@?ed9zP_VxQr4%N9tQZ|>x)B|yj(vDN_+wm@XyAw}Mm>`lFQ!fL~UXqg; zo$eXk6d!5s{HE%ojkKNm<~Ln+(}*8R!*y5bi=rD1dZ#x}0d>F|vnlkXMqILM2x+16 z3LW^fuWund;g$uimJV^w$UJN%s{X*|?3MG}DYS*qBfz8N3Y&=0=aeu8Qhnc0+vTOZ zfX@l0Xj%4y)M1`C?k&?*Klxy}An&Ea#PdaFuclK6g%JbI&+B-}2kv^JQ+reSEtPn; z*?I_@7xNgH2n^^(wehnWzC8>CdJp2q=-Uc5ek-nVNqy&ui_*Ko zo|0+w!>!((eOa`iQf1*fAFSZ+jn8L;_{sI3CT99kFanbur+XQxhijcH;p!E#2b$Mb z>WXU$MRalnafA=!S2Z)P(cRhKX9&5YL(Sd@-Q6@8^~f-I^8EuD_ku}8^VzjVG^+i4 zO08BE+7bjf@aQdzbnQvae=K57M>$doFCXnZ1M)xDiym@gT9Wxcp@ zK6hV`0p8G{rT6o*N|n+Tv*PDc1X7v8?X>BeWHBfIs`st3kd7+yKpE#PXlH3JNF?*BWQu7379}}l5wo2PYcY4GfKG?~0mqUIg*?l*A zevfXjdcvr$drIZ|U+M2{jJbncueH7=++EZ3 zQR}E#FFuYo8setG?{2UzCf#`zWfevt^nE%78|wd~vBKt1%dEzWM1EPyKSSJfd;1tY zN(5o&s~wxGtbTsJa8#V)&7(sExRw$Ez+ZYI<9&3-Qci0xTnM-%Ejz!uxl*7AVDmMtV{o1Z;JG@Ddxwtd;8J)3HO&8uC*1W8;}IEp94SbL8p-*W_G} z_epfO1=AMN?hJ^DJ#%Is{T-rvop(V)vcY+Wwi&NXA3t{z4`opj_3}2#6Cx!Hgx@hX zPCNH`_IqkVNP`eM`Qgo;(AAsGu@4)(^-b!Hu2L~RiQpwRjXt9Fky#m#KLI)i+ul(e zpL?DR|K4UUtzcJbpTJQ2AFPo{-F;Yt>vmP=t=m>HpHf1curtB|7;OLgTtBs0?h6hF zGm3V0-1$MtzG-C7=cfx(Q>#=7&MKTouU+|XA_#Bb`@0Q)JYo5evevcQvY)q`f4=U} zeVy-_bxf5+>*PZ*Cx?+hfX|>hFvcPYLWb&oG(~?pZGffIK=9Icv(@rfHaK&)Lqf(G z$`2S&S$6QnQWXl})suV=DO{BRE-{3}_QPK`QoL}dha?*A?-$=cXbMbxjRVn>CAYM8 z_b`$U4Wp)(VlGF(b*F5820I;M%Z|hS#@?jzZhWiZ`IIdn_Im;cLgHjdUtoZU8~fqs zvO<*dII%+_BKA15Qw~dsyaL4qD4CH=k0c%41+(rTyEzYLD5nlb1Xje>R?HG_5CK#x zd6*zA1i?;ZnyA4WUrC0q%+ESl6w@9qXQl}s_1-W@6=-ANLu7Hexu^u+V-L8?vX>OCP7aCX0L6KGgSFTqVQsopOASdKsnjSK#?ca@;3VwB^oTBzYpv+%8*N zN=0AQb9$5dgln(jcWCA?KVQ89vMF-68@0!WiMZ3Og_ePjdI~tFQ#iqRKogt^m;WWB z%Z@B&;n^{B5ZB1;J6!K2TGOAtl29r81UPTNZ+gvY^<6~7x1Ef<3SZiadlW=Dbw;Vt zko}HTYP#>YQ}DUt0$=7`<@b9iJob%0C?-Mw?slOt70ntA`;kI583XcK9mjK+BMu-? zRu;LY5TauN{5CBvW%+VqfY0rkRKJ$p=9<*fANx*_o4{s@`?r^EOVBs2^@u?_tXk&6 zy22hclgSm5_*{iSX*M56!RJgUOW$u#J`UC-eCenI1P+%E?&))gPo%zxR=BMZ-aZ&< zv|{WaH$;pE+NUx;A%Rq{D=mGvRYhEz-VtFbMEgX*@dHylCWSq&Xhi+v^f zi$`&&LjLT=SxHGE5Vov!WO(!lRcNMNUmxz~TkNv^rH+g_^s~R;=QqLS@@i9EGi3dx zDRGB>+|fYuo!6y6UqGz z&*?~HGWRrYipF%(iDyqYO#TojrWKmHEw4mtYZIU$2BcIFGF<^i{qHZIygA)ZIj{vU zUb%Rw1wlWmo9^gzDPUZF0|0O0(?&dC^Z}lKS3JQ#zaI*A)`~>C-)uM zTC-#ChBCVds(ymg-k8}CTMxd^Do7xdADpx+sGg_pVuhZ|3Vj=NJP-ALT5(e~S2H&K zQ|)~V)H-2e_1+R;L}ts~MB_ z$Ik-cj|skvkvYqc79TejV-6BbQ~4KJP0gy!V|Nl0C}h>nam}aCpU6$iyL|W}nlkFQ zZ-WhjDHVX3pu~Q2rr+k7;zuz+!Y=5W-8y$;y22ExGAT>b`MVXCC7wwf0@KzEoMu92 z1^{z|Ur(`&UmE9WZX|>w=JZ)4)vbI5F)y2mN(ui`jYMZd$F{R-+A9nsC&X(ZNdw{O zkFPt{K)Q{ik0p<+{GVE#eW%tlVK+L+7!sABIcG4S=pT>p-S(IYAu`tF;B zeka|^TlF~EPTz*`%s+lwsR4G&^fd*IUuCeT76+9>5?|p6cPdH{Kg}wnjkOE7H=Mbz z8QyuCv0Z&V!Ez9(@(BXq(r_-x&J)Dw3hPj5;*)|t0N&Yb@)Uwu_a#4~SGYs0jA^7Qj+s_s7 zn!IvOor0!MnSNHm4VI+OoEuwM==r4qI8rNfJ zRz}?V#zxLyTwj@|I!95m;QgZV!0&GaZAs!7cyH~Z*;+n(Q5ep0%RWeWS_@-oGy z^JriFZ1mQe{fiU2HsR*$Gc5%u)a$mP$31%7V%GRY_}Z%V83`GRID2hSy+<8y`h%Rz z#LsW~7Dt@9O+!ftYdy1ei4b2Ann%ZumMiKg4lT0sa)jv^5TG|T_IyCHEn$kNG;)_& z8ZxKq#02=kbJh~nR3!@)QlM$AEWjecbzW0JpmL7!DQ3I@Clb>9K19<6mmn#9z(Z3a zQa?J}mbO_NZl(AARgGrS%F2JcAl zwC638s=EkYSOp~FL;0x!r*A+9tJ*Q`7WemYzf5c8kLl#*&NaHPrUywnOd`&#iPo!^ z*I2_l^Dv^RKm`D-_7@78(*g6x_&+sRD9^&jbMm2*96>CA|ES1FbbgYz^bYO~3~!9D zEU8qib}&-Wrt7Z?p99vZ<_}lODM+(N<kVL*2(?8Zp-Lx~WvhwwlYrKN%M<4W;(5}Ord|H_s z;g6Tr=HBQM8XjjD&WIAsF^K=R87U-hZl2MO?or<`;>J4?O?cY%fJ}c@HZihaTagjZ zUOvFZ$KzU(H;qm%&1_Nro;!8O*WUDe>AOH+w94*XCf~TU>F>-T@fOT5)Jf#;O)-FxSu=SZUxS6-<;-D`snNKAb`HIiV) z7PM!-w5!^OdL1)=V3y#-4W%_D*btdQmGwU&#S)F=#JxSjGB}ZKHUHRZy-&njKUUp< zuBmQl=+!A+yz;}aHIKSEWF(+-$|ZIsD`CXp>$OG_K-ll+_>TzgHuq>t4_-Z?t1Kf$ zt9_zP4yI4PF*cb5$!)3OJ|iegU%ob09TC)W)o&1!CP86{*Him-1TE*z`0Z8dlVcwr zIu$~*H%vO+u4N-1KF;SM{2!_hYn*mW(`U%6+Ph3w4MR1O?)@|{KS+2aJcM*z5Mfvc zB)UfTbb=WOqJWd(pWnrYS*%Wp7r!@_5Hi_RjYdo>gDg!+_%|L)=`EaC7B%lDXT34E!;9XJO7Vw7;1*a49zmu2^$2?9jJB_9_#Xndc)G z7+cRM*h+hT_3dnLM!)Sbs@l0;>cr?Va04>5+ioH>Kf+zxQu?Ll<_N#-PTD}FxA@Lk zI*+iwNS{b$T+Hg(r92y(r8Ji7bh4YVvkqd}}yEp!cu$b;9E*4a7eU9vCTkdTFYOUfUg0 z2GWmRtNqq_nF5#}CLxzl3-&l0z~$E<_~)0uCjfwpuR-v@uR%WYUdJUH?49Tz8tfSC zZW}PpcQ=0^FF>3d$`-)Vi}+1}Z)2*`+DJ0=-kLzOdGhy-h81Q~#VJYVlfVihr=DLn zdXDuXmDX0W4lM6G8d#@Vv<#gYejL|^b!i+GeR|Z}aBa<){f9`_M%7Z^x5#r{*x|Q1 ztD-6dUYA=@o@4(!R@W((U*x3}t-eie!eB#VD#t^Sj?V~7Tn;TN3{4?BcH z^_St^;%(C7Y^S#l`wn!~eJ*?OoeuiR&*$Q`>Lxd@J&2>^UCK>u4lBQyE`Ych-OI=*)WhUIcw9mmRFN^c6(A%TF~7pQyJ)vdS2D|JT|31fZf;MpRsRq%+!ua_1%midzLo~XK`jrJMT}(;9_9IrU6g%3qNaIZ-zZE zVUnrgUQc~t=BYMmIh&CV{q>bY_rBh-8*FJ{JpGXD#mxQluQJ|K7W-wNP6y3t zz=I^8_msiII`icd@27-69>0>sZLch#&Z~M5pD8Z#YkD|a94QdF{AE7mQG=w9$g0AZ z&9A$%jGF;VGs!=QE241MECZV2xNA@Idx9EAIyH}L9?n>jt{N&fXWz)0qzu!4L6oxo zig^CJGKuto+u?D9y5EvSnuOm_J_SsXn#|Ipw)z*)do)5bT&(k|gln^6#4T1gD0~*m z>#Vl&Hv94S*B$`_;#S&bvQ+dlwDRZ|+U;1eOs@^E1l7u4d@c;(e6!PwtthEJe8**$ z4aX`D!J`MFfG{xsj(2bPaCMgHoxr`F{9BS~O4B@~#Ba2(Xc#yO<1K}@!nKhyI7g=B zi1pYer@8oi%KWh#FJ&pue;j^gu<{AH`{1~*Mb?619uv!9pZ(Tv(9Oa7RG{WbXO!<^ zCC@;#vr8a!^3&=t3s(3sQLU=pJYqzxYUxPT8#G1`1(Vo9rgMEznP*l z>0ASdZd$^l6V<>huBdu}VfcvLl~q&Kg@n6j{kz018>Gn2UR7E!i=JzOFTjyKXRIct+ z@va-}H4b(=CRN(ny<;oUO)7Y!|IUJN$Mc)RnZu;;EzQT3B5szO+DydXoO`bAht?P8 z3=(43XPMh+exIpdKd?TeJrJPuoW?U#QkG?3q}+D4nl*RxXUJ_j^`cIr%r40{eKSN; zuh)xq(`^au@rJOVy0eBl+Rf6a3noeyw6cC2^;p{OCaizJ5{y~}So-PT)_X22>&flx z$~y3|N>t>EGSpcSFO}Z6l3fk}B*D|dkv=9)IeJ4AII|(XQlDos-u!~{$*;Ap+Ee$f zRQ|i0mCwAdJgz!8_{v~+Z}2FWP$}xWhR2z_91DkGojY62>Go!)u$@m!2h9k=_S;(z z6}4FMvhRkTLqa?=4s%?D_8`+F!tNwK07>g>w?$+RGmr&&&`}+hpuLf2mvp{+LE6XrKjV<(axeE38hJ2lh4ODISB@Bch znNR#I4HX}F+;*+Pk;7%CpNmi!;T8wL3t2NOBh607hJ7ET`IMN<`Pj<4dwWY_=6FlK z4Qo18yF2%_Rsb|$;wEgYtG?7$~u`mx3tDPrZ77CpdCo$jbLEk^y>%h*$~Gwi<9J%PL+T!ybdUW zCuKHk?$0ulo*N-|`Mj}#MyqdBljK?VW|(l z)!kWMvG$_vtKYj&TmE0^nYGEgae2{sUdOqk{1dtJ99EU}>d9=?#>TI3Cb(Qwo2X6-7&Ms4y((h-;vqzc1fekS+dz*|a722hZO&WE%^g#f> z&4v`^OxhTHY7k+HrUW)Ik)g&~iEzN=BkBd35ai+@HK*)PcOy zTXn{ouTvLhUPT*Ko`mze99ebnqjFw-E|C=#uJYI5(%Yj%7de-GBw0Tpc&L5uL(6Z4 z@&0sAavXPNyzj?CLgU0_h)3?NVu&)DZldia9 z%*XzEuUo2H>)WKX&Yd3ADA^D>sOWd5UudBG+O0i{zxzQc+oN};c*eHBeM;Ploms8u zo4QM0$j>*q{=I> zv>ob>#y)6nAeVMlh?|oPHvxZyNH3(fRUv7kZ^-zTu70~hyuPXWX(=wSv*B>Q@D>-i zm(`!&m8#Y?@Il0Xe81xQ?+RVh+K|N?Mb=liGyaNj+^*pS t!xuk_z+UJGtD?E#Ctr7!aR5}As7hD!R&6zn)3*Z^&8-CHyS2=~{{gN1_Fw=2 literal 0 HcmV?d00001 diff --git a/packages/pinball_audio/lib/gen/assets.gen.dart b/packages/pinball_audio/lib/gen/assets.gen.dart new file mode 100644 index 00000000..3609b939 --- /dev/null +++ b/packages/pinball_audio/lib/gen/assets.gen.dart @@ -0,0 +1,69 @@ +/// GENERATED CODE - DO NOT MODIFY BY HAND +/// ***************************************************** +/// FlutterGen +/// ***************************************************** + +import 'package:flutter/widgets.dart'; + +class $AssetsSfxGen { + const $AssetsSfxGen(); + + String get google => 'assets/sfx/google.ogg'; + String get plim => 'assets/sfx/plim.ogg'; +} + +class Assets { + Assets._(); + + static const $AssetsSfxGen sfx = $AssetsSfxGen(); +} + +class AssetGenImage extends AssetImage { + const AssetGenImage(String assetName) + : super(assetName, package: 'pinball_audio'); + + Image image({ + Key? key, + ImageFrameBuilder? frameBuilder, + ImageLoadingBuilder? loadingBuilder, + ImageErrorWidgetBuilder? errorBuilder, + String? semanticLabel, + bool excludeFromSemantics = false, + double? width, + double? height, + Color? color, + BlendMode? colorBlendMode, + BoxFit? fit, + AlignmentGeometry alignment = Alignment.center, + ImageRepeat repeat = ImageRepeat.noRepeat, + Rect? centerSlice, + bool matchTextDirection = false, + bool gaplessPlayback = false, + bool isAntiAlias = false, + FilterQuality filterQuality = FilterQuality.low, + }) { + return Image( + key: key, + image: this, + frameBuilder: frameBuilder, + loadingBuilder: loadingBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + width: width, + height: height, + color: color, + colorBlendMode: colorBlendMode, + fit: fit, + alignment: alignment, + repeat: repeat, + centerSlice: centerSlice, + matchTextDirection: matchTextDirection, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + filterQuality: filterQuality, + ); + } + + String get path => assetName; +} diff --git a/packages/pinball_audio/lib/pinball_audio.dart b/packages/pinball_audio/lib/pinball_audio.dart new file mode 100644 index 00000000..a72ce165 --- /dev/null +++ b/packages/pinball_audio/lib/pinball_audio.dart @@ -0,0 +1,3 @@ +library pinball_audio; + +export 'src/pinball_audio.dart'; diff --git a/packages/pinball_audio/lib/src/pinball_audio.dart b/packages/pinball_audio/lib/src/pinball_audio.dart new file mode 100644 index 00000000..b2875084 --- /dev/null +++ b/packages/pinball_audio/lib/src/pinball_audio.dart @@ -0,0 +1,71 @@ +import 'package:audioplayers/audioplayers.dart'; +import 'package:flame_audio/audio_pool.dart'; +import 'package:flame_audio/flame_audio.dart'; +import 'package:pinball_audio/gen/assets.gen.dart'; + +/// Function that defines the contract of the creation +/// of an [AudioPool] +typedef CreateAudioPool = Future Function( + String sound, { + bool? repeating, + int? maxPlayers, + int? minPlayers, + String? prefix, +}); + +/// Function that defines the contract for playing a single +/// audio +typedef PlaySingleAudio = Future Function(String); + +/// Function that defines the contract for configuring +/// an [AudioCache] instance +typedef ConfigureAudioCache = void Function(AudioCache); + +/// {@template pinball_audio} +/// Sound manager for the pinball game +/// {@endtemplate} +class PinballAudio { + /// {@macro pinball_audio} + PinballAudio({ + CreateAudioPool? createAudioPool, + PlaySingleAudio? playSingleAudio, + ConfigureAudioCache? configureAudioCache, + }) : _createAudioPool = createAudioPool ?? AudioPool.create, + _playSingleAudio = playSingleAudio ?? FlameAudio.audioCache.play, + _configureAudioCache = configureAudioCache ?? + ((AudioCache a) { + a.prefix = ''; + }); + + final CreateAudioPool _createAudioPool; + + final PlaySingleAudio _playSingleAudio; + + final ConfigureAudioCache _configureAudioCache; + + late AudioPool _scorePool; + + /// Loads the sounds effects into the memory + Future load() async { + _configureAudioCache(FlameAudio.audioCache); + _scorePool = await _createAudioPool( + _prefixFile(Assets.sfx.plim), + maxPlayers: 4, + prefix: '', + ); + } + + /// Plays the basic score sound + void score() { + _scorePool.start(); + } + + /// Plays the google word bonus + void googleBonus() { + _playSingleAudio(_prefixFile(Assets.sfx.google)); + } + + String _prefixFile(String file) { + return 'packages/pinball_audio/$file'; + } +} diff --git a/packages/pinball_audio/pubspec.yaml b/packages/pinball_audio/pubspec.yaml new file mode 100644 index 00000000..a34ba5b5 --- /dev/null +++ b/packages/pinball_audio/pubspec.yaml @@ -0,0 +1,28 @@ +name: pinball_audio +description: Package with the sound manager for the pinball game +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ">=2.16.0 <3.0.0" + +dependencies: + audioplayers: ^0.20.1 + flame_audio: ^1.0.1 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + mocktail: ^0.3.0 + very_good_analysis: ^2.4.0 + +flutter_gen: + line_length: 80 + assets: + package_parameter_enabled: true + +flutter: + assets: + - assets/sfx/ diff --git a/packages/pinball_audio/test/helpers/helpers.dart b/packages/pinball_audio/test/helpers/helpers.dart new file mode 100644 index 00000000..efe914f6 --- /dev/null +++ b/packages/pinball_audio/test/helpers/helpers.dart @@ -0,0 +1 @@ +export 'mocks.dart'; diff --git a/packages/pinball_audio/test/helpers/mocks.dart b/packages/pinball_audio/test/helpers/mocks.dart new file mode 100644 index 00000000..c80fe65b --- /dev/null +++ b/packages/pinball_audio/test/helpers/mocks.dart @@ -0,0 +1,34 @@ +// ignore_for_file: one_member_abstracts + +import 'package:audioplayers/audioplayers.dart'; +import 'package:flame_audio/audio_pool.dart'; +import 'package:mocktail/mocktail.dart'; + +abstract class _CreateAudioPoolStub { + Future onCall( + String sound, { + bool? repeating, + int? maxPlayers, + int? minPlayers, + String? prefix, + }); +} + +class CreateAudioPoolStub extends Mock implements _CreateAudioPoolStub {} + +abstract class _ConfigureAudioCacheStub { + void onCall(AudioCache cache); +} + +class ConfigureAudioCacheStub extends Mock implements _ConfigureAudioCacheStub { +} + +abstract class _PlaySingleAudioStub { + Future onCall(String url); +} + +class PlaySingleAudioStub extends Mock implements _PlaySingleAudioStub {} + +class MockAudioPool extends Mock implements AudioPool {} + +class MockAudioCache extends Mock implements AudioCache {} diff --git a/packages/pinball_audio/test/src/pinball_audio_test.dart b/packages/pinball_audio/test/src/pinball_audio_test.dart new file mode 100644 index 00000000..2efe9553 --- /dev/null +++ b/packages/pinball_audio/test/src/pinball_audio_test.dart @@ -0,0 +1,110 @@ +// ignore_for_file: prefer_const_constructors +import 'package:flame_audio/flame_audio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_audio/gen/assets.gen.dart'; +import 'package:pinball_audio/pinball_audio.dart'; + +import '../helpers/helpers.dart'; + +void main() { + group('PinballAudio', () { + test('can be instantiated', () { + expect(PinballAudio(), isNotNull); + }); + + late CreateAudioPoolStub createAudioPool; + late ConfigureAudioCacheStub configureAudioCache; + late PlaySingleAudioStub playSingleAudio; + late PinballAudio audio; + + setUpAll(() { + registerFallbackValue(MockAudioCache()); + }); + + setUp(() { + createAudioPool = CreateAudioPoolStub(); + when( + () => createAudioPool.onCall( + any(), + maxPlayers: any(named: 'maxPlayers'), + prefix: any(named: 'prefix'), + ), + ).thenAnswer((_) async => MockAudioPool()); + + configureAudioCache = ConfigureAudioCacheStub(); + when(() => configureAudioCache.onCall(any())).thenAnswer((_) {}); + + playSingleAudio = PlaySingleAudioStub(); + when(() => playSingleAudio.onCall(any())).thenAnswer((_) async {}); + + audio = PinballAudio( + configureAudioCache: configureAudioCache.onCall, + createAudioPool: createAudioPool.onCall, + playSingleAudio: playSingleAudio.onCall, + ); + }); + + group('load', () { + test('creates the score pool', () async { + await audio.load(); + + verify( + () => createAudioPool.onCall( + 'packages/pinball_audio/${Assets.sfx.plim}', + maxPlayers: 4, + prefix: '', + ), + ).called(1); + }); + + test('configures the audio cache instance', () async { + await audio.load(); + + verify(() => configureAudioCache.onCall(FlameAudio.audioCache)) + .called(1); + }); + + test('sets the correct prefix', () async { + audio = PinballAudio( + createAudioPool: createAudioPool.onCall, + playSingleAudio: playSingleAudio.onCall, + ); + await audio.load(); + + expect(FlameAudio.audioCache.prefix, equals('')); + }); + }); + + group('score', () { + test('plays the score sound pool', () async { + final audioPool = MockAudioPool(); + when(audioPool.start).thenAnswer((_) async => () {}); + when( + () => createAudioPool.onCall( + any(), + maxPlayers: any(named: 'maxPlayers'), + prefix: any(named: 'prefix'), + ), + ).thenAnswer((_) async => audioPool); + + await audio.load(); + audio.score(); + + verify(audioPool.start).called(1); + }); + }); + + group('googleBonus', () { + test('plays the correct file', () async { + await audio.load(); + audio.googleBonus(); + + verify( + () => playSingleAudio + .onCall('packages/pinball_audio/${Assets.sfx.google}'), + ).called(1); + }); + }); + }); +} diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index c928df79..48ddbaff 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -3,16 +3,12 @@ /// FlutterGen /// ***************************************************** -// ignore_for_file: directives_ordering,unnecessary_import - import 'package:flutter/widgets.dart'; class $AssetsImagesGen { const $AssetsImagesGen(); - /// File path: assets/images/ball.png AssetGenImage get ball => const AssetGenImage('assets/images/ball.png'); - $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); $AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen(); $AssetsImagesChromeDinoGen get chromeDino => @@ -21,11 +17,8 @@ class $AssetsImagesGen { const $AssetsImagesDashBumperGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); - - /// File path: assets/images/flutter_sign_post.png AssetGenImage get flutterSignPost => const AssetGenImage('assets/images/flutter_sign_post.png'); - $AssetsImagesLaunchRampGen get launchRamp => const $AssetsImagesLaunchRampGen(); $AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen(); @@ -34,11 +27,8 @@ class $AssetsImagesGen { class $AssetsImagesBaseboardGen { const $AssetsImagesBaseboardGen(); - /// File path: assets/images/baseboard/left.png AssetGenImage get left => const AssetGenImage('assets/images/baseboard/left.png'); - - /// File path: assets/images/baseboard/right.png AssetGenImage get right => const AssetGenImage('assets/images/baseboard/right.png'); } @@ -46,11 +36,8 @@ class $AssetsImagesBaseboardGen { class $AssetsImagesBoundaryGen { const $AssetsImagesBoundaryGen(); - /// File path: assets/images/boundary/bottom.png AssetGenImage get bottom => const AssetGenImage('assets/images/boundary/bottom.png'); - - /// File path: assets/images/boundary/outer.png AssetGenImage get outer => const AssetGenImage('assets/images/boundary/outer.png'); } @@ -58,11 +45,8 @@ class $AssetsImagesBoundaryGen { class $AssetsImagesChromeDinoGen { const $AssetsImagesChromeDinoGen(); - /// File path: assets/images/chrome_dino/head.png AssetGenImage get head => const AssetGenImage('assets/images/chrome_dino/head.png'); - - /// File path: assets/images/chrome_dino/mouth.png AssetGenImage get mouth => const AssetGenImage('assets/images/chrome_dino/mouth.png'); } @@ -79,11 +63,8 @@ class $AssetsImagesDashBumperGen { class $AssetsImagesDinoGen { const $AssetsImagesDinoGen(); - /// File path: assets/images/dino/dino-land-bottom.png AssetGenImage get dinoLandBottom => const AssetGenImage('assets/images/dino/dino-land-bottom.png'); - - /// File path: assets/images/dino/dino-land-top.png AssetGenImage get dinoLandTop => const AssetGenImage('assets/images/dino/dino-land-top.png'); } @@ -91,11 +72,8 @@ class $AssetsImagesDinoGen { class $AssetsImagesFlipperGen { const $AssetsImagesFlipperGen(); - /// File path: assets/images/flipper/left.png AssetGenImage get left => const AssetGenImage('assets/images/flipper/left.png'); - - /// File path: assets/images/flipper/right.png AssetGenImage get right => const AssetGenImage('assets/images/flipper/right.png'); } @@ -103,11 +81,8 @@ class $AssetsImagesFlipperGen { class $AssetsImagesLaunchRampGen { const $AssetsImagesLaunchRampGen(); - /// File path: assets/images/launch_ramp/foreground-railing.png AssetGenImage get foregroundRailing => const AssetGenImage('assets/images/launch_ramp/foreground-railing.png'); - - /// File path: assets/images/launch_ramp/ramp.png AssetGenImage get ramp => const AssetGenImage('assets/images/launch_ramp/ramp.png'); } @@ -115,16 +90,12 @@ class $AssetsImagesLaunchRampGen { class $AssetsImagesSpaceshipGen { const $AssetsImagesSpaceshipGen(); - /// File path: assets/images/spaceship/bridge.png AssetGenImage get bridge => const AssetGenImage('assets/images/spaceship/bridge.png'); - $AssetsImagesSpaceshipRailGen get rail => const $AssetsImagesSpaceshipRailGen(); $AssetsImagesSpaceshipRampGen get ramp => const $AssetsImagesSpaceshipRampGen(); - - /// File path: assets/images/spaceship/saucer.png AssetGenImage get saucer => const AssetGenImage('assets/images/spaceship/saucer.png'); } @@ -132,11 +103,8 @@ class $AssetsImagesSpaceshipGen { class $AssetsImagesDashBumperAGen { const $AssetsImagesDashBumperAGen(); - /// File path: assets/images/dash_bumper/a/active.png AssetGenImage get active => const AssetGenImage('assets/images/dash_bumper/a/active.png'); - - /// File path: assets/images/dash_bumper/a/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/dash_bumper/a/inactive.png'); } @@ -144,11 +112,8 @@ class $AssetsImagesDashBumperAGen { class $AssetsImagesDashBumperBGen { const $AssetsImagesDashBumperBGen(); - /// File path: assets/images/dash_bumper/b/active.png AssetGenImage get active => const AssetGenImage('assets/images/dash_bumper/b/active.png'); - - /// File path: assets/images/dash_bumper/b/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/dash_bumper/b/inactive.png'); } @@ -156,11 +121,8 @@ class $AssetsImagesDashBumperBGen { class $AssetsImagesDashBumperMainGen { const $AssetsImagesDashBumperMainGen(); - /// File path: assets/images/dash_bumper/main/active.png AssetGenImage get active => const AssetGenImage('assets/images/dash_bumper/main/active.png'); - - /// File path: assets/images/dash_bumper/main/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/dash_bumper/main/inactive.png'); } @@ -168,11 +130,8 @@ class $AssetsImagesDashBumperMainGen { class $AssetsImagesSpaceshipRailGen { const $AssetsImagesSpaceshipRailGen(); - /// File path: assets/images/spaceship/rail/foreground.png AssetGenImage get foreground => const AssetGenImage('assets/images/spaceship/rail/foreground.png'); - - /// File path: assets/images/spaceship/rail/main.png AssetGenImage get main => const AssetGenImage('assets/images/spaceship/rail/main.png'); } @@ -180,15 +139,10 @@ class $AssetsImagesSpaceshipRailGen { class $AssetsImagesSpaceshipRampGen { const $AssetsImagesSpaceshipRampGen(); - /// File path: assets/images/spaceship/ramp/main.png AssetGenImage get main => const AssetGenImage('assets/images/spaceship/ramp/main.png'); - - /// File path: assets/images/spaceship/ramp/railing-background.png AssetGenImage get railingBackground => const AssetGenImage( 'assets/images/spaceship/ramp/railing-background.png'); - - /// File path: assets/images/spaceship/ramp/railing-foreground.png AssetGenImage get railingForeground => const AssetGenImage( 'assets/images/spaceship/ramp/railing-foreground.png'); } diff --git a/pubspec.lock b/pubspec.lock index ada9db4e..240c5a9f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,6 +29,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.8.2" + audioplayers: + dependency: transitive + description: + name: audioplayers + url: "https://pub.dartlang.org" + source: hosted + version: "0.20.1" bloc: dependency: "direct main" description: @@ -148,6 +155,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" file: dependency: transitive description: @@ -183,6 +197,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + flame_audio: + dependency: transitive + description: + name: flame_audio + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" flame_bloc: dependency: "direct main" description: @@ -259,6 +280,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.2" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.4" http_multi_server: dependency: transitive description: @@ -392,6 +420,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + path_provider: + dependency: transitive + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.12" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.5" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + pinball_audio: + dependency: "direct main" + description: + path: "packages/pinball_audio" + relative: true + source: path + version: "1.0.0+1" pinball_components: dependency: "direct main" description: @@ -406,6 +490,13 @@ packages: relative: true source: path version: "1.0.0+1" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" plugin_platform_interface: dependency: transitive description: @@ -420,6 +511,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.5.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" provider: dependency: transitive description: @@ -509,6 +607,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0+2" term_glyph: dependency: transitive description: @@ -544,6 +649,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.6" vector_math: dependency: transitive description: @@ -586,6 +698,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.5.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+1" yaml: dependency: transitive description: @@ -595,4 +721,4 @@ packages: version: "3.1.0" sdks: dart: ">=2.16.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.0" diff --git a/pubspec.yaml b/pubspec.yaml index a0cca553..161afb85 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,8 @@ dependencies: intl: ^0.17.0 leaderboard_repository: path: packages/leaderboard_repository + pinball_audio: + path: packages/pinball_audio pinball_components: path: packages/pinball_components pinball_theme: diff --git a/test/app/view/app_test.dart b/test/app/view/app_test.dart index f8415a58..01b5fea6 100644 --- a/test/app/view/app_test.dart +++ b/test/app/view/app_test.dart @@ -9,20 +9,26 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/app/app.dart'; import 'package:pinball/landing/landing.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import '../../helpers/mocks.dart'; void main() { group('App', () { late LeaderboardRepository leaderboardRepository; + late PinballAudio pinballAudio; setUp(() { leaderboardRepository = MockLeaderboardRepository(); + pinballAudio = MockPinballAudio(); }); testWidgets('renders LandingPage', (tester) async { await tester.pumpWidget( - App(leaderboardRepository: leaderboardRepository), + App( + leaderboardRepository: leaderboardRepository, + pinballAudio: pinballAudio, + ), ); expect(find.byType(LandingPage), findsOneWidget); }); diff --git a/test/game/components/bonus_word_test.dart b/test/game/components/bonus_word_test.dart index 7d73b6bc..6b1af085 100644 --- a/test/game/components/bonus_word_test.dart +++ b/test/game/components/bonus_word_test.dart @@ -4,9 +4,11 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flame/effects.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import '../../helpers/helpers.dart'; @@ -89,6 +91,21 @@ void main() { }, ); + flameTester.test( + 'plays the google bonus sound', + (game) async { + when(() => state.bonusHistory).thenReturn([GameBonus.word]); + + final bonusWord = BonusWord(position: Vector2.zero()); + await game.ensureAdd(bonusWord); + await game.ready(); + + bonusWord.onNewState(state); + + verify(bonusWord.gameRef.audio.googleBonus).called(1); + }, + ); + flameTester.test( 'adds a color effect to reset the color when the sequence is finished', (game) async { @@ -195,11 +212,15 @@ void main() { group('bonus letter activation', () { late GameBloc gameBloc; + late PinballAudio pinballAudio; final flameBlocTester = FlameBlocTester( // TODO(alestiago): Use TestGame once BonusLetter has controller. gameBuilder: PinballGameTest.create, blocBuilder: () => gameBloc, + repositories: () => [ + RepositoryProvider.value(value: pinballAudio), + ], ); setUp(() { @@ -209,6 +230,9 @@ void main() { const Stream.empty(), initialState: const GameState.initial(), ); + + pinballAudio = MockPinballAudio(); + when(pinballAudio.googleBonus).thenAnswer((_) {}); }); flameBlocTester.testGameWidget( diff --git a/test/game/components/score_points_test.dart b/test/game/components/score_points_test.dart index f97bdada..8317f20c 100644 --- a/test/game/components/score_points_test.dart +++ b/test/game/components/score_points_test.dart @@ -2,6 +2,7 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; import '../../helpers/helpers.dart'; @@ -20,6 +21,7 @@ void main() { group('BallScorePointsCallback', () { late PinballGame game; late GameBloc bloc; + late PinballAudio audio; late Ball ball; late FakeScorePoints fakeScorePoints; @@ -27,6 +29,7 @@ void main() { game = MockPinballGame(); bloc = MockGameBloc(); ball = MockBall(); + audio = MockPinballAudio(); fakeScorePoints = FakeScorePoints(); }); @@ -38,7 +41,8 @@ void main() { test( 'emits Scored event with points', () { - when(game.read).thenReturn(bloc); + when(game.read).thenReturn(bloc); + when(() => game.audio).thenReturn(audio); BallScorePointsCallback(game).begin( ball, @@ -53,6 +57,22 @@ void main() { ).called(1); }, ); + + test( + 'plays a Score sound', + () { + when(game.read).thenReturn(bloc); + when(() => game.audio).thenReturn(audio); + + BallScorePointsCallback(game).begin( + ball, + fakeScorePoints, + FakeContact(), + ); + + verify(audio.score).called(1); + }, + ); }); }); } diff --git a/test/helpers/builders.dart b/test/helpers/builders.dart index f78aebe7..d0eea644 100644 --- a/test/helpers/builders.dart +++ b/test/helpers/builders.dart @@ -7,13 +7,19 @@ class FlameBlocTester> FlameBlocTester({ required GameCreateFunction gameBuilder, required B Function() blocBuilder, + List Function()? repositories, }) : super( gameBuilder, pumpWidget: (gameWidget, tester) async { await tester.pumpWidget( BlocProvider.value( value: blocBuilder(), - child: gameWidget, + child: repositories == null + ? gameWidget + : MultiRepositoryProvider( + providers: repositories.call(), + child: gameWidget, + ), ), ); }, diff --git a/test/helpers/extensions.dart b/test/helpers/extensions.dart index b3c4c6f8..a5039381 100644 --- a/test/helpers/extensions.dart +++ b/test/helpers/extensions.dart @@ -1,6 +1,8 @@ import 'package:pinball/game/game.dart'; import 'package:pinball_theme/pinball_theme.dart'; +import 'helpers.dart'; + /// [PinballGame] extension to reduce boilerplate in tests. extension PinballGameTest on PinballGame { /// Create [PinballGame] with default [PinballTheme]. @@ -8,6 +10,7 @@ extension PinballGameTest on PinballGame { theme: const PinballTheme( characterTheme: DashTheme(), ), + audio: MockPinballAudio(), )..images.prefix = ''; } @@ -18,5 +21,6 @@ extension DebugPinballGameTest on DebugPinballGame { theme: const PinballTheme( characterTheme: DashTheme(), ), + audio: MockPinballAudio(), ); } diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index f139bc7b..c0dec5f5 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -8,6 +8,7 @@ import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/leaderboard/leaderboard.dart'; import 'package:pinball/theme/theme.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; class MockPinballGame extends Mock implements PinballGame {} @@ -69,3 +70,5 @@ class MockFixture extends Mock implements Fixture {} class MockComponentSet extends Mock implements ComponentSet {} class MockDashNestBumper extends Mock implements DashNestBumper {} + +class MockPinballAudio extends Mock implements PinballAudio {} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index d5e819b4..722dc44c 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -14,9 +14,18 @@ import 'package:mockingjay/mockingjay.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/theme/theme.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'helpers.dart'; +PinballAudio _buildDefaultPinballAudio() { + final audio = MockPinballAudio(); + + when(audio.load).thenAnswer((_) => Future.value()); + + return audio; +} + extension PumpApp on WidgetTester { Future pumpApp( Widget widget, { @@ -24,31 +33,41 @@ extension PumpApp on WidgetTester { GameBloc? gameBloc, ThemeCubit? themeCubit, LeaderboardRepository? leaderboardRepository, + PinballAudio? pinballAudio, }) { - return pumpWidget( - RepositoryProvider.value( - value: leaderboardRepository ?? MockLeaderboardRepository(), - child: MultiBlocProvider( + return runAsync(() { + return pumpWidget( + MultiRepositoryProvider( providers: [ - BlocProvider.value( - value: themeCubit ?? MockThemeCubit(), + RepositoryProvider.value( + value: leaderboardRepository ?? MockLeaderboardRepository(), ), - BlocProvider.value( - value: gameBloc ?? MockGameBloc(), + RepositoryProvider.value( + value: pinballAudio ?? _buildDefaultPinballAudio(), ), ], - child: MaterialApp( - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, + child: MultiBlocProvider( + providers: [ + BlocProvider.value( + value: themeCubit ?? MockThemeCubit(), + ), + BlocProvider.value( + value: gameBloc ?? MockGameBloc(), + ), ], - supportedLocales: AppLocalizations.supportedLocales, - home: navigator != null - ? MockNavigatorProvider(navigator: navigator, child: widget) - : widget, + child: MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: navigator != null + ? MockNavigatorProvider(navigator: navigator, child: widget) + : widget, + ), ), ), - ), - ); + ); + }); } } From d25643ce9ab7226aa16481f231c9d64855a09516 Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Tue, 5 Apr 2022 11:12:26 -0500 Subject: [PATCH 2/5] fix: component render order (#142) * fix: simplify and order priorities * test: remove position --- lib/game/components/board.dart | 2 +- lib/game/components/plunger.dart | 1 - lib/game/pinball_game.dart | 18 ++++-------------- .../lib/src/components/dino_walls.dart | 13 +++++-------- .../lib/src/components/launch_ramp.dart | 3 +-- .../test/src/components/dino_walls_test.dart | 3 +-- 6 files changed, 12 insertions(+), 28 deletions(-) diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart index e71d5ede..49595f10 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -8,7 +8,7 @@ import 'package:pinball_components/pinball_components.dart'; class Board extends Component { /// {@macro board} // TODO(alestiago): Make Board a Blueprint and sort out priorities. - Board() : super(priority: 5); + Board() : super(priority: 1); @override Future onLoad() async { diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index cc5797c0..9b7eec39 100644 --- a/lib/game/components/plunger.dart +++ b/lib/game/components/plunger.dart @@ -15,7 +15,6 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition { Plunger({ required this.compressionDistance, }) : super( - priority: 5, // TODO(allisonryan0002): remove paint after asset is added. paint: Paint()..color = const Color.fromARGB(255, 241, 8, 8), ); diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index e09ab461..1fa99b8c 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -35,11 +35,13 @@ class PinballGame extends Forge2DGame _addContactCallbacks(); await _addGameBoundaries(); - unawaited(add(Board())); unawaited(addFromBlueprint(Boundaries())); + unawaited(addFromBlueprint(LaunchRamp())); unawaited(_addPlunger()); + unawaited(add(Board())); + unawaited(addFromBlueprint(DinoWalls())); unawaited(_addBonusWord()); - unawaited(_addRamps()); + unawaited(addFromBlueprint(SpaceshipRamp())); unawaited( addFromBlueprint( Spaceship( @@ -68,13 +70,6 @@ class PinballGame extends Forge2DGame Future _addGameBoundaries() async { await add(BottomWall()); createBoundaries(this).forEach(add); - unawaited( - addFromBlueprint( - DinoWalls( - position: Vector2(-2.4, 0), - ), - ), - ); } Future _addPlunger() async { @@ -95,11 +90,6 @@ class PinballGame extends Forge2DGame ); } - Future _addRamps() async { - unawaited(addFromBlueprint(SpaceshipRamp())); - unawaited(addFromBlueprint(LaunchRamp())); - } - void spawnBall() { final ball = ControlledBall.launch( theme: theme, diff --git a/packages/pinball_components/lib/src/components/dino_walls.dart b/packages/pinball_components/lib/src/components/dino_walls.dart index 13f56ff3..daf83850 100644 --- a/packages/pinball_components/lib/src/components/dino_walls.dart +++ b/packages/pinball_components/lib/src/components/dino_walls.dart @@ -12,16 +12,13 @@ import 'package:pinball_components/pinball_components.dart' hide Assets; /// {@endtemplate} class DinoWalls extends Forge2DBlueprint { /// {@macro dinowalls} - DinoWalls({required this.position}); - - /// The [position] where the elements will be created - final Vector2 position; + DinoWalls(); @override void build(_) { addAll([ - _DinoTopWall()..initialPosition = position, - _DinoBottomWall()..initialPosition = position, + _DinoTopWall(), + _DinoBottomWall(), ]); } } @@ -31,7 +28,7 @@ class DinoWalls extends Forge2DBlueprint { /// {@endtemplate} class _DinoTopWall extends BodyComponent with InitialPosition { ///{@macro dino_top_wall} - _DinoTopWall() : super(priority: 2); + _DinoTopWall() : super(priority: 1); List _createFixtureDefs() { final fixturesDef = []; @@ -129,7 +126,7 @@ class _DinoTopWall extends BodyComponent with InitialPosition { /// {@endtemplate} class _DinoBottomWall extends BodyComponent with InitialPosition { ///{@macro dino_top_wall} - _DinoBottomWall() : super(priority: 2); + _DinoBottomWall() : super(priority: 1); List _createFixtureDefs() { final fixturesDef = []; diff --git a/packages/pinball_components/lib/src/components/launch_ramp.dart b/packages/pinball_components/lib/src/components/launch_ramp.dart index 5f7ee098..3268cc46 100644 --- a/packages/pinball_components/lib/src/components/launch_ramp.dart +++ b/packages/pinball_components/lib/src/components/launch_ramp.dart @@ -142,7 +142,7 @@ class _LaunchRampBase extends BodyComponent with InitialPosition, Layered { class _LaunchRampForegroundRailing extends BodyComponent with InitialPosition, Layered { /// {@macro launch_ramp_foreground_railing} - _LaunchRampForegroundRailing() : super(priority: 4) { + _LaunchRampForegroundRailing() : super(priority: 1) { layer = Layer.launcher; } @@ -207,7 +207,6 @@ class _LaunchRampForegroundRailing extends BodyComponent size: sprite.originalSize / 10, anchor: Anchor.center, position: Vector2(22.8, 0), - priority: 4, ), ); } diff --git a/packages/pinball_components/test/src/components/dino_walls_test.dart b/packages/pinball_components/test/src/components/dino_walls_test.dart index af80444b..bb85bc8e 100644 --- a/packages/pinball_components/test/src/components/dino_walls_test.dart +++ b/packages/pinball_components/test/src/components/dino_walls_test.dart @@ -1,6 +1,5 @@ // ignore_for_file: cascade_invocations -import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -15,7 +14,7 @@ void main() { flameTester.test( 'loads correctly', (game) async { - final dinoWalls = DinoWalls(position: Vector2.zero()); + final dinoWalls = DinoWalls(); await game.addFromBlueprint(dinoWalls); await game.ready(); From 39b04c1393aa37a54f859425993690e5bfe808bf Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Tue, 5 Apr 2022 18:36:19 +0200 Subject: [PATCH 3/5] feat: kicker assets and dimension (#144) * refactor: added kicker asset and fixed dimensions and sizes * refactor: loadsprite for kicker * test: added golden test for kicker * feat: included Kicker sandbox * refactor: analyzer fixes Co-authored-by: alestiago --- lib/game/components/board.dart | 4 +- .../assets/images/kicker/left.png | Bin 0 -> 14253 bytes .../assets/images/kicker/right.png | Bin 0 -> 13917 bytes .../lib/gen/assets.gen.dart | 59 ++++++++++++++++++ .../lib/src/components/kicker.dart | 47 +++++++++----- packages/pinball_components/pubspec.yaml | 1 + .../pinball_components/sandbox/lib/main.dart | 2 + .../lib/stories/kicker/kicker_game.dart | 39 ++++++++++++ .../sandbox/lib/stories/kicker/stories.dart | 17 +++++ .../test/src/components/golden/kickers.png | Bin 0 -> 60136 bytes .../test/src/components/kicker_test.dart | 28 ++++++++- 11 files changed, 176 insertions(+), 21 deletions(-) create mode 100644 packages/pinball_components/assets/images/kicker/left.png create mode 100644 packages/pinball_components/assets/images/kicker/right.png create mode 100644 packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/kicker/stories.dart create mode 100644 packages/pinball_components/test/src/components/golden/kickers.png diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart index 49595f10..a312daee 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -83,8 +83,8 @@ class _BottomGroupSide extends Component { final kicker = Kicker( side: _side, )..initialPosition = Vector2( - (22.0 * direction) + centerXAdjustment, - -26, + (22.4 * direction) + centerXAdjustment, + -25, ); await addAll([flipper, baseboard, kicker]); diff --git a/packages/pinball_components/assets/images/kicker/left.png b/packages/pinball_components/assets/images/kicker/left.png new file mode 100644 index 0000000000000000000000000000000000000000..42bd5030e8b4acf1d86d9f8fd2839e2744feac18 GIT binary patch literal 14253 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 const AssetGenImage('assets/images/ball.png'); + $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); $AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen(); $AssetsImagesChromeDinoGen get chromeDino => @@ -17,8 +21,12 @@ class $AssetsImagesGen { const $AssetsImagesDashBumperGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); + + /// File path: assets/images/flutter_sign_post.png AssetGenImage get flutterSignPost => const AssetGenImage('assets/images/flutter_sign_post.png'); + + $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); $AssetsImagesLaunchRampGen get launchRamp => const $AssetsImagesLaunchRampGen(); $AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen(); @@ -27,8 +35,11 @@ class $AssetsImagesGen { class $AssetsImagesBaseboardGen { const $AssetsImagesBaseboardGen(); + /// File path: assets/images/baseboard/left.png AssetGenImage get left => const AssetGenImage('assets/images/baseboard/left.png'); + + /// File path: assets/images/baseboard/right.png AssetGenImage get right => const AssetGenImage('assets/images/baseboard/right.png'); } @@ -36,8 +47,11 @@ class $AssetsImagesBaseboardGen { class $AssetsImagesBoundaryGen { const $AssetsImagesBoundaryGen(); + /// File path: assets/images/boundary/bottom.png AssetGenImage get bottom => const AssetGenImage('assets/images/boundary/bottom.png'); + + /// File path: assets/images/boundary/outer.png AssetGenImage get outer => const AssetGenImage('assets/images/boundary/outer.png'); } @@ -45,8 +59,11 @@ class $AssetsImagesBoundaryGen { class $AssetsImagesChromeDinoGen { const $AssetsImagesChromeDinoGen(); + /// File path: assets/images/chrome_dino/head.png AssetGenImage get head => const AssetGenImage('assets/images/chrome_dino/head.png'); + + /// File path: assets/images/chrome_dino/mouth.png AssetGenImage get mouth => const AssetGenImage('assets/images/chrome_dino/mouth.png'); } @@ -63,8 +80,11 @@ class $AssetsImagesDashBumperGen { class $AssetsImagesDinoGen { const $AssetsImagesDinoGen(); + /// File path: assets/images/dino/dino-land-bottom.png AssetGenImage get dinoLandBottom => const AssetGenImage('assets/images/dino/dino-land-bottom.png'); + + /// File path: assets/images/dino/dino-land-top.png AssetGenImage get dinoLandTop => const AssetGenImage('assets/images/dino/dino-land-top.png'); } @@ -72,17 +92,35 @@ class $AssetsImagesDinoGen { class $AssetsImagesFlipperGen { const $AssetsImagesFlipperGen(); + /// File path: assets/images/flipper/left.png AssetGenImage get left => const AssetGenImage('assets/images/flipper/left.png'); + + /// File path: assets/images/flipper/right.png AssetGenImage get right => const AssetGenImage('assets/images/flipper/right.png'); } +class $AssetsImagesKickerGen { + const $AssetsImagesKickerGen(); + + /// File path: assets/images/kicker/left.png + AssetGenImage get left => + const AssetGenImage('assets/images/kicker/left.png'); + + /// File path: assets/images/kicker/right.png + AssetGenImage get right => + const AssetGenImage('assets/images/kicker/right.png'); +} + class $AssetsImagesLaunchRampGen { const $AssetsImagesLaunchRampGen(); + /// File path: assets/images/launch_ramp/foreground-railing.png AssetGenImage get foregroundRailing => const AssetGenImage('assets/images/launch_ramp/foreground-railing.png'); + + /// File path: assets/images/launch_ramp/ramp.png AssetGenImage get ramp => const AssetGenImage('assets/images/launch_ramp/ramp.png'); } @@ -90,12 +128,16 @@ class $AssetsImagesLaunchRampGen { class $AssetsImagesSpaceshipGen { const $AssetsImagesSpaceshipGen(); + /// File path: assets/images/spaceship/bridge.png AssetGenImage get bridge => const AssetGenImage('assets/images/spaceship/bridge.png'); + $AssetsImagesSpaceshipRailGen get rail => const $AssetsImagesSpaceshipRailGen(); $AssetsImagesSpaceshipRampGen get ramp => const $AssetsImagesSpaceshipRampGen(); + + /// File path: assets/images/spaceship/saucer.png AssetGenImage get saucer => const AssetGenImage('assets/images/spaceship/saucer.png'); } @@ -103,8 +145,11 @@ class $AssetsImagesSpaceshipGen { class $AssetsImagesDashBumperAGen { const $AssetsImagesDashBumperAGen(); + /// File path: assets/images/dash_bumper/a/active.png AssetGenImage get active => const AssetGenImage('assets/images/dash_bumper/a/active.png'); + + /// File path: assets/images/dash_bumper/a/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/dash_bumper/a/inactive.png'); } @@ -112,8 +157,11 @@ class $AssetsImagesDashBumperAGen { class $AssetsImagesDashBumperBGen { const $AssetsImagesDashBumperBGen(); + /// File path: assets/images/dash_bumper/b/active.png AssetGenImage get active => const AssetGenImage('assets/images/dash_bumper/b/active.png'); + + /// File path: assets/images/dash_bumper/b/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/dash_bumper/b/inactive.png'); } @@ -121,8 +169,11 @@ class $AssetsImagesDashBumperBGen { class $AssetsImagesDashBumperMainGen { const $AssetsImagesDashBumperMainGen(); + /// File path: assets/images/dash_bumper/main/active.png AssetGenImage get active => const AssetGenImage('assets/images/dash_bumper/main/active.png'); + + /// File path: assets/images/dash_bumper/main/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/dash_bumper/main/inactive.png'); } @@ -130,8 +181,11 @@ class $AssetsImagesDashBumperMainGen { class $AssetsImagesSpaceshipRailGen { const $AssetsImagesSpaceshipRailGen(); + /// File path: assets/images/spaceship/rail/foreground.png AssetGenImage get foreground => const AssetGenImage('assets/images/spaceship/rail/foreground.png'); + + /// File path: assets/images/spaceship/rail/main.png AssetGenImage get main => const AssetGenImage('assets/images/spaceship/rail/main.png'); } @@ -139,10 +193,15 @@ class $AssetsImagesSpaceshipRailGen { class $AssetsImagesSpaceshipRampGen { const $AssetsImagesSpaceshipRampGen(); + /// File path: assets/images/spaceship/ramp/main.png AssetGenImage get main => const AssetGenImage('assets/images/spaceship/ramp/main.png'); + + /// File path: assets/images/spaceship/ramp/railing-background.png AssetGenImage get railingBackground => const AssetGenImage( 'assets/images/spaceship/ramp/railing-background.png'); + + /// File path: assets/images/spaceship/ramp/railing-foreground.png AssetGenImage get railingForeground => const AssetGenImage( 'assets/images/spaceship/ramp/railing-foreground.png'); } diff --git a/packages/pinball_components/lib/src/components/kicker.dart b/packages/pinball_components/lib/src/components/kicker.dart index d9eb7932..442f4200 100644 --- a/packages/pinball_components/lib/src/components/kicker.dart +++ b/packages/pinball_components/lib/src/components/kicker.dart @@ -1,10 +1,10 @@ import 'dart:math' as math; -import 'package:flame/extensions.dart'; +import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; import 'package:geometry/geometry.dart' as geometry show centroid; -import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/gen/assets.gen.dart'; +import 'package:pinball_components/pinball_components.dart' hide Assets; /// {@template kicker} /// Triangular [BodyType.static] body that propels the [Ball] towards the @@ -16,12 +16,7 @@ class Kicker extends BodyComponent with InitialPosition { /// {@macro kicker} Kicker({ required BoardSide side, - }) : _side = side { - // TODO(alestiago): Use sprite instead of color when provided. - paint = Paint() - ..color = const Color(0xFF00FF00) - ..style = PaintingStyle.fill; - } + }) : _side = side; /// Whether the [Kicker] is on the left or right side of the board. /// @@ -31,24 +26,22 @@ class Kicker extends BodyComponent with InitialPosition { final BoardSide _side; /// The size of the [Kicker] body. - // TODO(alestiago): Use size from PositionedBodyComponent instead, - // once a sprite is given. - static final Vector2 size = Vector2(4, 10); + static final Vector2 size = Vector2(4.4, 15); List _createFixtureDefs() { final fixturesDefs = []; final direction = _side.direction; const quarterPi = math.pi / 4; - final upperCircle = CircleShape()..radius = 1.45; + final upperCircle = CircleShape()..radius = 1.6; upperCircle.position.setValues(0, -upperCircle.radius / 2); final upperCircleFixtureDef = FixtureDef(upperCircle)..friction = 0; fixturesDefs.add(upperCircleFixtureDef); - final lowerCircle = CircleShape()..radius = 1.45; + final lowerCircle = CircleShape()..radius = 1.6; lowerCircle.position.setValues( size.x * -direction, - -size.y, + -size.y - 0.8, ); final lowerCircleFixtureDef = FixtureDef(lowerCircle)..friction = 0; fixturesDefs.add(lowerCircleFixtureDef); @@ -60,8 +53,7 @@ class Kicker extends BodyComponent with InitialPosition { upperCircle.radius * direction, 0, ), - // TODO(alestiago): Use values from design. - Vector2(2.0 * direction, -size.y + 2), + Vector2(2.5 * direction, -size.y + 2), ); final wallFacingLineFixtureDef = FixtureDef(wallFacingEdge)..friction = 0; fixturesDefs.add(wallFacingLineFixtureDef); @@ -125,6 +117,27 @@ class Kicker extends BodyComponent with InitialPosition { return body; } + + @override + Future onLoad() async { + await super.onLoad(); + renderBody = false; + + final sprite = await gameRef.loadSprite( + (_side.isLeft) + ? Assets.images.kicker.left.keyName + : Assets.images.kicker.right.keyName, + ); + + await add( + SpriteComponent( + sprite: sprite, + size: Vector2(8.7, 19), + anchor: Anchor.center, + position: Vector2(0.7 * -_side.direction, -2.2), + ), + ); + } } // TODO(alestiago): Evaluate if there's value on generalising this to diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index 0e5eb37a..c7302d0d 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -38,6 +38,7 @@ flutter: - assets/images/spaceship/rail/ - assets/images/spaceship/ramp/ - assets/images/chrome_dino/ + - assets/images/kicker/ flutter_gen: line_length: 80 diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 59066fca..1801fa52 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -6,6 +6,7 @@ // https://opensource.org/licenses/MIT. import 'package:dashbook/dashbook.dart'; import 'package:flutter/material.dart'; +import 'package:sandbox/stories/kicker/stories.dart'; import 'package:sandbox/stories/stories.dart'; void main() { @@ -19,5 +20,6 @@ void main() { addBaseboardStories(dashbook); addChromeDinoStories(dashbook); addDashNestBumperStories(dashbook); + addKickerStories(dashbook); runApp(dashbook); } diff --git a/packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart b/packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart new file mode 100644 index 00000000..21c0cfb8 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart @@ -0,0 +1,39 @@ +import 'package:flame/extensions.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class KickerGame extends BasicBallGame { + KickerGame({ + required this.trace, + }) : super(color: const Color(0xFFFF0000)); + + static const info = ''' + Shows how Kickers are rendered. + + - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a ball into the game. +'''; + + final bool trace; + + @override + Future onLoad() async { + await super.onLoad(); + + final center = screenToWorld(camera.viewport.canvasSize! / 2); + + final leftKicker = Kicker(side: BoardSide.left) + ..initialPosition = Vector2(center.x - (Kicker.size.x * 2), center.y); + await add(leftKicker); + + final rightKicker = Kicker(side: BoardSide.right) + ..initialPosition = Vector2(center.x + (Kicker.size.x * 2), center.y); + await add(rightKicker); + + if (trace) { + leftKicker.trace(); + rightKicker.trace(); + } + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart b/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart new file mode 100644 index 00000000..f4a6bf91 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart @@ -0,0 +1,17 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/game.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/kicker/kicker_game.dart'; + +void addKickerStories(Dashbook dashbook) { + dashbook.storiesOf('Kickers').add( + 'Basic', + (context) => GameWidget( + game: KickerGame( + trace: context.boolProperty('Trace', true), + ), + ), + codeLink: buildSourceLink('kicker_game/basic.dart'), + info: KickerGame.info, + ); +} diff --git a/packages/pinball_components/test/src/components/golden/kickers.png b/packages/pinball_components/test/src/components/golden/kickers.png new file mode 100644 index 0000000000000000000000000000000000000000..231769238f77b05d239a6940780237d1c317f0d3 GIT binary patch literal 60136 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, + // matchesGoldenFile('golden/kickers.png'), + // ); + //}, + ); flameTester.test( 'loads correctly', From 625e033709e14c95f36fee4c51cbd3e032c811d8 Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Tue, 5 Apr 2022 11:51:57 -0500 Subject: [PATCH 4/5] chore: preload kicker assets (#146) --- lib/game/game_assets.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 6bafe37e..47175c32 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -13,6 +13,8 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.flipper.right.keyName), images.load(components.Assets.images.baseboard.left.keyName), images.load(components.Assets.images.baseboard.right.keyName), + images.load(components.Assets.images.kicker.left.keyName), + images.load(components.Assets.images.kicker.right.keyName), images.load(components.Assets.images.launchRamp.ramp.keyName), images.load( components.Assets.images.launchRamp.foregroundRailing.keyName, From 1a8f534ececca84972e65a6dfdd8e498438d13cf Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 5 Apr 2022 21:08:06 +0100 Subject: [PATCH 5/5] refactor: test with an empty game (#147) * refactor: used EmptyPinballGame * refactor: removed extension * refactor: used EmptyPinballGame * fix: merge conflicts --- lib/game/pinball_game.dart | 14 ++-- test/game/components/board_test.dart | 3 +- test/game/components/bonus_word_test.dart | 77 +++++++++++-------- .../game/components/controlled_ball_test.dart | 8 +- .../components/controlled_flipper_test.dart | 2 +- test/game/components/flutter_forest_test.dart | 18 ++--- test/game/pinball_game_test.dart | 4 +- test/helpers/extensions.dart | 39 +++++----- 8 files changed, 93 insertions(+), 72 deletions(-) diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 1fa99b8c..3c99fbca 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -22,8 +22,6 @@ class PinballGame extends Forge2DGame final PinballAudio audio; - late final Plunger plunger; - @override void onAttach() { super.onAttach(); @@ -73,7 +71,7 @@ class PinballGame extends Forge2DGame } Future _addPlunger() async { - plunger = Plunger(compressionDistance: 29) + final plunger = Plunger(compressionDistance: 29) ..initialPosition = BoardDimensions.bounds.center.toVector2() + Vector2(41.5, -49); await add(plunger); @@ -90,14 +88,20 @@ class PinballGame extends Forge2DGame ); } - void spawnBall() { + Future spawnBall() async { + // TODO(alestiago): Remove once this logic is moved to controller. + var plunger = firstChild(); + if (plunger == null) { + await add(plunger = Plunger(compressionDistance: 1)); + } + final ball = ControlledBall.launch( theme: theme, )..initialPosition = Vector2( plunger.body.position.x, plunger.body.position.y + Ball.size.y, ); - add(ball); + await add(ball); } } diff --git a/test/game/components/board_test.dart b/test/game/components/board_test.dart index 2f51b2b1..9f2a5260 100644 --- a/test/game/components/board_test.dart +++ b/test/game/components/board_test.dart @@ -9,7 +9,7 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(PinballGameTest.create); + final flameTester = FlameTester(EmptyPinballGameTest.new); group('Board', () { flameTester.test( @@ -78,7 +78,6 @@ void main() { flameTester.test( 'one FlutterForest', (game) async { - // TODO(alestiago): change to [NestBumpers] once provided. final board = Board(); await game.ready(); await game.ensureAdd(board); diff --git a/test/game/components/bonus_word_test.dart b/test/game/components/bonus_word_test.dart index 6b1af085..f01fced9 100644 --- a/test/game/components/bonus_word_test.dart +++ b/test/game/components/bonus_word_test.dart @@ -14,16 +14,18 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(PinballGameTest.create); + final flameTester = FlameTester(EmptyPinballGameTest.new); group('BonusWord', () { flameTester.test( 'loads the letters correctly', (game) async { - await game.ready(); + final bonusWord = BonusWord( + position: Vector2.zero(), + ); + await game.ensureAdd(bonusWord); - final bonusWord = game.children.whereType().first; - final letters = bonusWord.children.whereType(); + final letters = bonusWord.descendants().whereType(); expect(letters.length, equals(GameBloc.bonusWord.length)); }, ); @@ -135,7 +137,7 @@ void main() { }); group('BonusLetter', () { - final flameTester = FlameTester(PinballGameTest.create); + final flameTester = FlameTester(EmptyPinballGameTest.new); flameTester.test( 'loads correctly', @@ -215,8 +217,7 @@ void main() { late PinballAudio pinballAudio; final flameBlocTester = FlameBlocTester( - // TODO(alestiago): Use TestGame once BonusLetter has controller. - gameBuilder: PinballGameTest.create, + gameBuilder: EmptyPinballGameTest.new, blocBuilder: () => gameBloc, repositories: () => [ RepositoryProvider.value(value: pinballAudio), @@ -238,14 +239,20 @@ void main() { flameBlocTester.testGameWidget( 'adds BonusLetterActivated to GameBloc when not activated', setUp: (game, tester) async { - await game.ready(); - final bonusLetter = game.descendants().whereType().first; + final bonusWord = BonusWord( + position: Vector2.zero(), + ); + await game.ensureAdd(bonusWord); - bonusLetter.activate(); - await game.ready(); - }, - verify: (game, tester) async { - verify(() => gameBloc.add(const BonusLetterActivated(0))).called(1); + final bonusLetters = + game.descendants().whereType().toList(); + for (var index = 0; index < bonusLetters.length; index++) { + final bonusLetter = bonusLetters[index]; + bonusLetter.activate(); + await game.ready(); + + verify(() => gameBloc.add(BonusLetterActivated(index))).called(1); + } }, ); @@ -309,25 +316,33 @@ void main() { ); flameBlocTester.testGameWidget( - 'only listens when there is a change on the letter status', + 'listens when there is a change on the letter status', setUp: (game, tester) async { - await game.ready(); - final bonusLetter = game.descendants().whereType().first; - bonusLetter.activate(); - }, - verify: (game, tester) async { - const state = GameState( - score: 0, - balls: 2, - activatedBonusLetters: [0], - activatedDashNests: {}, - bonusHistory: [], - ); - final bonusLetter = game.descendants().whereType().first; - expect( - bonusLetter.listenWhen(const GameState.initial(), state), - isTrue, + final bonusWord = BonusWord( + position: Vector2.zero(), ); + await game.ensureAdd(bonusWord); + + final bonusLetters = + game.descendants().whereType().toList(); + for (var index = 0; index < bonusLetters.length; index++) { + final bonusLetter = bonusLetters[index]; + bonusLetter.activate(); + await game.ready(); + + final state = GameState( + score: 0, + balls: 2, + activatedBonusLetters: [index], + activatedDashNests: const {}, + bonusHistory: const [], + ); + + expect( + bonusLetter.listenWhen(const GameState.initial(), state), + isTrue, + ); + } }, ); }); diff --git a/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index f9494543..05056484 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -13,7 +13,7 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(PinballGameTest.create); + final flameTester = FlameTester(EmptyPinballGameTest.new); group('BonusBallController', () { late Ball ball; @@ -67,7 +67,7 @@ void main() { }); final flameBlocTester = FlameBlocTester( - gameBuilder: PinballGameTest.create, + gameBuilder: EmptyPinballGameTest.new, blocBuilder: () => gameBloc, ); @@ -155,13 +155,13 @@ void main() { await game.ensureAdd(ball); final state = MockGameState(); - when(() => state.balls).thenReturn(2); + when(() => state.balls).thenReturn(1); final previousBalls = game.descendants().whereType().toList(); controller.onNewState(state); await game.ready(); - final currentBalls = game.descendants().whereType(); + final currentBalls = game.descendants().whereType().toList(); expect(currentBalls.contains(ball), isFalse); expect(currentBalls.length, equals(previousBalls.length)); diff --git a/test/game/components/controlled_flipper_test.dart b/test/game/components/controlled_flipper_test.dart index eabeca5e..03c51830 100644 --- a/test/game/components/controlled_flipper_test.dart +++ b/test/game/components/controlled_flipper_test.dart @@ -10,7 +10,7 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(PinballGameTest.create); + final flameTester = FlameTester(EmptyPinballGameTest.new); group('FlipperController', () { group('onKeyEvent', () { diff --git a/test/game/components/flutter_forest_test.dart b/test/game/components/flutter_forest_test.dart index 33dbb991..60c55be9 100644 --- a/test/game/components/flutter_forest_test.dart +++ b/test/game/components/flutter_forest_test.dart @@ -25,7 +25,7 @@ void beginContact(Forge2DGame game, BodyComponent bodyA, BodyComponent bodyB) { void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(PinballGameTest.create); + final flameTester = FlameTester(EmptyPinballGameTest.new); group('FlutterForest', () { flameTester.test( @@ -146,16 +146,15 @@ void main() { }); final flameBlocTester = FlameBlocTester( - gameBuilder: PinballGameTest.create, + gameBuilder: EmptyPinballGameTest.new, blocBuilder: () => gameBloc, ); flameBlocTester.testGameWidget( 'add DashNestActivated event', setUp: (game, tester) async { - await game.ready(); - final flutterForest = - game.descendants().whereType().first; + final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); await game.ensureAdd(ball); final bumpers = @@ -177,15 +176,16 @@ void main() { final flutterForest = FlutterForest(); await game.ensureAdd(flutterForest); await game.ensureAdd(ball); + game.addContactCallback(BallScorePointsCallback(game)); - final bumpers = - flutterForest.descendants().whereType(); + final bumpers = flutterForest.descendants().whereType(); for (final bumper in bumpers) { beginContact(game, bumper, ball); - final points = (bumper as ScorePoints).points; verify( - () => gameBloc.add(Scored(points: points)), + () => gameBloc.add( + Scored(points: bumper.points), + ), ).called(1); } }, diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 52008074..f418bad0 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -12,8 +12,8 @@ import '../helpers/helpers.dart'; void main() { group('PinballGame', () { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(PinballGameTest.create); - final debugModeFlameTester = FlameTester(DebugPinballGameTest.create); + final flameTester = FlameTester(PinballGameTest.new); + final debugModeFlameTester = FlameTester(DebugPinballGameTest.new); // TODO(alestiago): test if [PinballGame] registers // [BallScorePointsCallback] once the following issue is resolved: diff --git a/test/helpers/extensions.dart b/test/helpers/extensions.dart index a5039381..4731eec4 100644 --- a/test/helpers/extensions.dart +++ b/test/helpers/extensions.dart @@ -3,24 +3,27 @@ import 'package:pinball_theme/pinball_theme.dart'; import 'helpers.dart'; -/// [PinballGame] extension to reduce boilerplate in tests. -extension PinballGameTest on PinballGame { - /// Create [PinballGame] with default [PinballTheme]. - static PinballGame create() => PinballGame( - theme: const PinballTheme( - characterTheme: DashTheme(), - ), - audio: MockPinballAudio(), - )..images.prefix = ''; +class PinballGameTest extends PinballGame { + PinballGameTest() + : super( + audio: MockPinballAudio(), + theme: const PinballTheme( + characterTheme: DashTheme(), + ), + ); } -/// [DebugPinballGame] extension to reduce boilerplate in tests. -extension DebugPinballGameTest on DebugPinballGame { - /// Create [PinballGame] with default [PinballTheme]. - static DebugPinballGame create() => DebugPinballGame( - theme: const PinballTheme( - characterTheme: DashTheme(), - ), - audio: MockPinballAudio(), - ); +class DebugPinballGameTest extends DebugPinballGame { + DebugPinballGameTest() + : super( + audio: MockPinballAudio(), + theme: const PinballTheme( + characterTheme: DashTheme(), + ), + ); +} + +class EmptyPinballGameTest extends PinballGameTest { + @override + Future onLoad() async {} }