From 3a593c2e6e4ddbe33f9c8597349ee08b0c572158 Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Mon, 2 May 2022 16:51:35 +0200 Subject: [PATCH 1/4] feat: multiball asset (#235) * feat: multiball assets * feat: added multiball to components * feat: added controller for multiball * feat: positioned multiball and changed animation * feat: added sandbox for multiball * chore: unused import * refactor: add rotation to multiball constructor * test: coverage for multiball * chore: todos for refactor multiball childrens * test: removed unused mock * chore: removed unused imports * test: removed golden tests * refactor: changed assets and refactored multiball * refactor: changed assets and refactored multiball * test: tests for multiball * refactor: multiballs group refactored * chore: names and doc * refactor: removed duplicated images for multiball * refactor: changed multiball cubit and state * refactor: changed multiball and group * chore: positions of lights * refactor: changing blink behavior * test: blink behavior * refactor: blinking multiball lights * test: tests for blink behavior * chore: analysis errors * test: coverage for blinking * test: coverage * test: trying to fix tests * fix: fixed bloc error on behavior with tests * refactor: multiball blink * refactor: blinking behavior to TimerComponent and test coverage * refactor: modified blinking behavior * chore: error on merge tests * test: coverage multiballs * refactor: cleaned blink behavior * chore: unused import * Update packages/pinball_components/test/src/components/multiball/behaviors/multiball_blinking_behavior_test.dart Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * Update packages/pinball_components/test/src/components/multiball/behaviors/multiball_blinking_behavior_test.dart Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * Update test/game/components/multiballs/behaviors/multiballs_behavior_test.dart Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * Update packages/pinball_components/test/src/components/multiball/multiball_test.dart Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * refactor: changed multiball states enum values * test: multiball descendant test at pinball Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- lib/game/components/components.dart | 1 + .../multiballs/behaviors/behaviors.dart | 1 + .../behaviors/multiballs_behavior.dart | 28 ++++ .../components/multiballs/multiballs.dart | 30 ++++ lib/game/game_assets.dart | 2 + lib/game/pinball_game.dart | 1 + .../assets/images/multiball/dimmed.png | Bin 0 -> 14979 bytes .../assets/images/multiball/lit.png | Bin 0 -> 15893 bytes .../lib/gen/assets.gen.dart | 13 ++ .../lib/src/components/components.dart | 1 + .../multiball/behaviors/behaviors.dart | 1 + .../multiball_blinking_behavior.dart | 78 +++++++++ .../multiball/cubit/multiball_cubit.dart | 37 ++++ .../multiball/cubit/multiball_state.dart | 44 +++++ .../src/components/multiball/multiball.dart | 138 +++++++++++++++ packages/pinball_components/pubspec.yaml | 1 + .../pinball_components/sandbox/lib/main.dart | 1 + .../lib/stories/multiball/multiball_game.dart | 56 +++++++ .../lib/stories/multiball/stories.dart | 11 ++ .../sandbox/lib/stories/stories.dart | 1 + .../test/helpers/mocks.dart | 2 + .../multiball_blinking_behavior_test.dart | 158 ++++++++++++++++++ .../multiball/cubit/multiball_cubit_test.dart | 67 ++++++++ .../multiball/cubit/multiball_state_test.dart | 76 +++++++++ .../components/multiball/multiball_test.dart | 90 ++++++++++ .../behaviors/multiballs_behavior_test.dart | 136 +++++++++++++++ .../multiballs/multiballs_test.dart | 54 ++++++ test/game/pinball_game_test.dart | 14 ++ test/helpers/mocks.dart | 4 +- 29 files changed, 1043 insertions(+), 3 deletions(-) create mode 100644 lib/game/components/multiballs/behaviors/behaviors.dart create mode 100644 lib/game/components/multiballs/behaviors/multiballs_behavior.dart create mode 100644 lib/game/components/multiballs/multiballs.dart create mode 100644 packages/pinball_components/assets/images/multiball/dimmed.png create mode 100644 packages/pinball_components/assets/images/multiball/lit.png create mode 100644 packages/pinball_components/lib/src/components/multiball/behaviors/behaviors.dart create mode 100644 packages/pinball_components/lib/src/components/multiball/behaviors/multiball_blinking_behavior.dart create mode 100644 packages/pinball_components/lib/src/components/multiball/cubit/multiball_cubit.dart create mode 100644 packages/pinball_components/lib/src/components/multiball/cubit/multiball_state.dart create mode 100644 packages/pinball_components/lib/src/components/multiball/multiball.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/multiball/multiball_game.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/multiball/stories.dart create mode 100644 packages/pinball_components/test/src/components/multiball/behaviors/multiball_blinking_behavior_test.dart create mode 100644 packages/pinball_components/test/src/components/multiball/cubit/multiball_cubit_test.dart create mode 100644 packages/pinball_components/test/src/components/multiball/cubit/multiball_state_test.dart create mode 100644 packages/pinball_components/test/src/components/multiball/multiball_test.dart create mode 100644 test/game/components/multiballs/behaviors/multiballs_behavior_test.dart create mode 100644 test/game/components/multiballs/multiballs_test.dart diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 5af4efc0..aeb5742e 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -10,6 +10,7 @@ export 'flutter_forest/flutter_forest.dart'; export 'game_flow_controller.dart'; export 'google_word/google_word.dart'; export 'launcher.dart'; +export 'multiballs/multiballs.dart'; export 'multipliers/multipliers.dart'; export 'scoring_behavior.dart'; export 'sparky_scorch.dart'; diff --git a/lib/game/components/multiballs/behaviors/behaviors.dart b/lib/game/components/multiballs/behaviors/behaviors.dart new file mode 100644 index 00000000..921063dc --- /dev/null +++ b/lib/game/components/multiballs/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'multiballs_behavior.dart'; diff --git a/lib/game/components/multiballs/behaviors/multiballs_behavior.dart b/lib/game/components/multiballs/behaviors/multiballs_behavior.dart new file mode 100644 index 00000000..8b323ff4 --- /dev/null +++ b/lib/game/components/multiballs/behaviors/multiballs_behavior.dart @@ -0,0 +1,28 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Toggle each [Multiball] when there is a bonus ball. +class MultiballsBehavior extends Component + with + HasGameRef, + ParentIsA, + BlocComponent { + @override + bool listenWhen(GameState? previousState, GameState newState) { + final hasChanged = previousState?.bonusHistory != newState.bonusHistory; + final lastBonusIsMultiball = newState.bonusHistory.isNotEmpty && + newState.bonusHistory.last == GameBonus.dashNest; + + return hasChanged && lastBonusIsMultiball; + } + + @override + void onNewState(GameState state) { + parent.children.whereType().forEach((multiball) { + multiball.bloc.onAnimate(); + }); + } +} diff --git a/lib/game/components/multiballs/multiballs.dart b/lib/game/components/multiballs/multiballs.dart new file mode 100644 index 00000000..04f6525a --- /dev/null +++ b/lib/game/components/multiballs/multiballs.dart @@ -0,0 +1,30 @@ +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/game/components/multiballs/behaviors/behaviors.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template multiballs_component} +/// A [SpriteGroupComponent] for the multiball over the board. +/// {@endtemplate} +class Multiballs extends Component with ZIndex { + /// {@macro multiballs_component} + Multiballs() + : super( + children: [ + Multiball.a(), + Multiball.b(), + Multiball.c(), + Multiball.d(), + MultiballsBehavior(), + ], + ) { + zIndex = ZIndexes.decal; + } + + /// Creates a [Multiballs] without any children. + /// + /// This can be used for testing [Multiballs]'s behaviors in isolation. + @visibleForTesting + Multiballs.test(); +} diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index d8532c50..76e1bddc 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -114,6 +114,8 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.googleWord.letter6.lit.keyName), images.load(components.Assets.images.googleWord.letter6.dimmed.keyName), images.load(components.Assets.images.backboard.display.keyName), + images.load(components.Assets.images.multiball.lit.keyName), + images.load(components.Assets.images.multiball.dimmed.keyName), images.load(components.Assets.images.multiplier.x2.lit.keyName), images.load(components.Assets.images.multiplier.x2.dimmed.keyName), images.load(components.Assets.images.multiplier.x3.lit.keyName), diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index cd5a1c7f..41005886 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -53,6 +53,7 @@ class PinballGame extends Forge2DGame final decals = [ GoogleWord(position: Vector2(-4.25, 1.8)), Multipliers(), + Multiballs(), ]; final characterAreas = [ AndroidAcres(), diff --git a/packages/pinball_components/assets/images/multiball/dimmed.png b/packages/pinball_components/assets/images/multiball/dimmed.png new file mode 100644 index 0000000000000000000000000000000000000000..f7d9407af3922c309f63fb63a29a57fe019f0fb8 GIT binary patch literal 14979 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 literal 0 HcmV?d00001 diff --git a/packages/pinball_components/assets/images/multiball/lit.png b/packages/pinball_components/assets/images/multiball/lit.png new file mode 100644 index 0000000000000000000000000000000000000000..3444309cc296e31fdec7486a2675567de9f41e83 GIT binary patch literal 15893 zcmZA8RZtw;76xE^aJS%ca1RcHYamE)*WeDp-3cDtCBfYZ?h@P~=-}=$_}t0s-CuS0 zb9e1pwd-H2zpEaaAUe9`gu>$taSt|bcJPP=ob^S6s@amS;3xyv=P-8K9{kzTUjN9C*zA{eavHpRQ^dKn)ejC|QknL=91Tpo zPY>8N_V0!mB13FpA;u`viuv>A_`Z)zIUJu+mKeEX%E%Ws1gsIt2XNJjfYkj04o-p1rP&>4#v-i{M#wTERh92^_r<5d=nV;@e({pb;{&PjPqeFE71qr) z8QPQ7Eo`+&6}QDoOSR8P(r2j0WKA5^-U`lsFG1i?r~iB^`Q7gZaoE?^yoDMEp26dP z0ND?{9t#Kt7-n1k?r_V|%uNiM zls8hFL_-d5?)N@e+WVNbjI6JF?@lTKtRGU{)^FFhMh7;Pc92~$qePG4L0%ZvNF}piMTn>+Ra{m=}Lm z^m`9+_R_BHL4-HPZ2EC&2jf}yx$+2CCvYY^#Sx*n1G4%{K5;X7Ny`Xe)0Fgw9=ijD zyFR2mqY+zN%*o@%7zTp-c4{??4ctg}YtD$+p1Qh^V{c?1qja8{6(2+I`1QbfAtUO1 zR_9(LR5KR74m;hCYFz(}wye$b;BVJ>9Q}b|ee+h*o4Wht<$9Y1hs(XuH{c7I=_XN< zwdDk3!`2-58}}xflL6Rudb~(&V4Zn_rtYXV*2$9+GzVc4dl!c6V_` zoyi$@yl$0xCGS!<_^KvQeFu`-_H`4#LjF}Q9U!!=f)?2A%u2w>&>FMwM3sNQ0?t?_ruqOBzb;vV+Ft4mq3R53auLBNO9N zEEYq+Qlo9l9!!k>n(ivuLjIlMlx_hTZ^!d<5I&314iEB|DsdP$;#1hW5m}b6SFPOL zMevV3E$iq#$eWR7o@?mN5E%?P{NEklZJpupOSE1Gs)i^(Y#!#9OZiWJY!12Hw?VXW z(A;lTpddRlZkY8Nxn4adp=-SNhRqvU18%PhV=l8Xu!TE|jESTLTl$(3wn=7SJB%V8 zGW8ZT1N(sE>f4m>3qSYP?maFSTHmx^ZZDrWu*4s`+5^E2dObT##Z^&9c2$eoELKVU zSyGcRthIs$ITP5Y?}HXQmpjPy>U$pa*?&%2skcvf#>J0|HRi#C9LS$=-0g}H-X5zw zUgs=1&J(72F`TgZ);~`0D`^Z2GdBYL_h(_vt5^mnJ1;W+JGAZUZI^r}VbOu#15d!4 z)PGW%5KwiJum2j-pkWX~I%qt24YLEcEfl0X{R%4{XolGbGg%@wfYi-kMWtNQ~jM12iUiT&fF07V}MPo7Tqo? zCCbZWU9|85eenDKZA{i?fG1cs|K0d}8R81lUs-JRkR#2&o(*v8)h>)S*^4nVjX^W=T#0qaQAWUoY$Q z%MRPIm3sy=0AE14@-PnX03AXw-5UKNm0HQKPQOYWQufoxNM5>k{Kkn}vDa21<{O&M zz<%)8;QJIcN&;5iq8=^@f2jaG*c67HX=JK=YzvrKd^jBO4E7ue82UzlIAr}14eu__ z(hmK~;wYD^cS{Rw7GxFsKt>MaZ3Y~H91kxg5xk!5%KN85WLC;ekkPl!3$<@?fCZuY z5@5H#`E9%Cl9~Z;`;CXL_ykeH_(9#lALunbY+gHRd1|I?G4os-sBHvVq93~*w zFdDiU0o9&tvRPYP(^YgM8S0-Y7wyOww4eQ#;A=&ao>5F&UUFMBzDP=ud#Vr6!uMEo~>9QobtrV;Hgp@042RFbP+1GhN3t8B%Zu;AOL+5s-0;PXycmLUz`Izcdt{ zae(!!Zh5bUd<|pSaTR~ATgA1ya`dd9os2xaz6~{R2&Qrsn=@;*`2aEh*lF;Tp2=td zV-Q*|#LA*E%c}#=w{6YxGzzzTT*iRx6g;2VTD|TjvB3W0;gG6)$gxtv+vmN)y}!XI z&$4eVuC=CwfH?gJMfl5At;#s^NI-u4Tiw_V_H8PGl3a=J0ADzTt!l@ERUzF?Qda&@ z3$h^9x9?B?*?KrLk`JhapJP~Js5I=gn zGNu7yvyQB*)m&RVf@w*W+>M5*D``*4YwwNbSyt_9|ICMs$s0w$vL7_oUSen^5g(_^PMO44=|pVKZl1-$-I)Uc)sg zkC&!xc-pr-zc%E9kA)Q;oEkR>OJ&deVf%s5e*9V4_%{FkHlI&i2V6wAk&OH^xH;0+ zB4{Qf(z5z4S&tkdu79^?7^45zD6Hy)P^UwtzqkO&MO(c`F$yKCmOZWw@7U$2D9pN# z0}j8AVeQwMsR?BDJ=LFY#q*yanR-S-0@i!hd$wb$%O9TOzP$|_H|Z9_jEp5a@WmA& zH`oj52Rh4a*nx~qhDK;R3|J56HE8|A3=iCm;?INbB{0}-;nI#FI{$U{!d#PXW5b|m z$iZCKV8fcwH=y?#*whTb{90g@fpIJP5RNj8-$+k#6qs45gED+7A!inVaJ!qr;J;C@ zRrRIkRN_hnOBGK)#3@PCAQr*+hsxC=!lmIqSDLp_fJ=vet|;!Hkieh8tl&}*y9-eV zTA%mChYKHMf;eVRq1)}Jkj|?yzJtbWV8G? z?Dt@&A-;Zg+G`0U-+iV7it_-|j#I@4;O@KFEjXVq=N z6*rpLS{zjYIwHtu{}z0+TD1mC;g0uEWAvdaz7`bHXWUIwT>KUepwS9SO5YgqK5d-? zx;4d5@h3akX%M)eu3lKr9(jm6kKF5u1%YQ7izqd8-58m_0;ZKX2Bw(cg8s2^kJb~H zB<22YQyOq->A;7i6Of4Z10-W4^B={Gy)2pdH5pi|J;*|5CgdsrH2V3ePTfkio##)M zDw5CVM3KK(PiUxMf*y<$eE05s0!C$aC`9w@`pFf{P0fEmqDN53A&gNW$a!GkJW#!_ zdl(Ao&TFH%+~n&~_$nSYN7B!8+z`0=v-$&_zu)d!oa$Z=9v8gV@52K`*Y&*@n`hIR znk>jSCe1nYP{to4>ToCh>BKlrGrh0#pI*c^;H5pvTML%eY)`xol(UfUCqzn^17<;; ziBi$cZs6xX>bpzfpTCP3x4Kk|2H%Epj0ZAr{OCdc80`r0x@E=3VA14B7A^Njk8G@Hd-d%o^O9tRrc+!+$_Bu=Ck~3+S8Bh(Ip34|fyi z9sQtKY#=cNyIrt@_uPYG3KQ6Iw{HI1u?OG7mQP13skB5aU|3O=RQ{4iGA7fdOz*KI&CafJ*Z{ufNd zh4uSFAjL@_37K)D&|?Dkd;mp!F}Z{L=Lk2uX-a`WN~0(auP+#6jbz+pjlG)Vse2WO zH;97L-7wGt{BHrc_55}*_dryM&4TGrOR4_3uwoR&!cHge{2Nhcr@vjr-~hyppKJHX zQA@wX5|*e;?I&_c_D_fih;m9pz)SBfXXnwaP5q)AuO&tYowpMz{N-Jx7md0&c=LhB zbQ(xRe7_g%TmP&>zQHs}4Fe83BwGY~&#KAf$(ev$qICd%G<5aQ+q0fU(NuxDc3vu` zUedt-JiJV(OgYXtmIPY+cQX@+_7@^bPxId&c;Jr2eTBq56rfO#YEPgf9ZT2Q+j7N4 zN+>iF2FiU)FLK)fo>uF*E}ls8lf?&WZXk3k0r+bDCdb)!L>^ZA$A)cgFXy+u`WI5r zSvt|$1|ytHWSYhktTYy00FPVYR>=Z3Zr@IlBX!?jCKaS4td}`%jvL|ydqF~!kQRMj z4eaxNnxJHdMm%p={^?Gj1denqS}YyTT+K-L1tpA9HyGWU3wGg>tQ4*%qDmcnXt@!w z;o!-!wn3s$k*>k<9af#2R~>$fe9Lf)T(zAsH1(ppCtNH)-+a6+794baNS)^mOz=L^A+7smD?O0YSW0igY3H35vtV^@_!++_!-4N654=?o{>UQyKDABY>TQ*P z;Ng)4mS2w+)bfkoxP(9FXQMq&B9UwA97DI;@?9y>jpy((=hv0>+pjH(YCxda zKT+&4P@zSh$?rez*hkm)q3MM)x?ATlF;bsOh+BwTuQ_-H=mA1e@q(+t_y>E5^K3d% z0U48P^$PQC_jH(eGfM)`2%q5Ln;=OIQ|`g%>rIDl*_-iYKQ{`}90BSa?C5vEE<|{z zCy?sfl>mM!*5r=~!c&49Oq2`Oq}Lug5ncJ!HqwiA?tGL24?-=1 z-2mSbk?}l~xvhVwn(*~d*(9O}%))iB?L(C6f`!L(_oqU}_1Pib-e`LhQ<=HkqI(ie18+Y7SIHXd=4R{hj)Z_X59Apd2d*5UxADWP#|gyl{MLTM-#Yqu*XU)ihLn<+w-ANQm0LE@_nTp>rGa zW4L^Y!ZB5G+OjLZ+6WYY*`cG>8#T%1jKG?3-7o%90ik|~zQj$!11=wWe>tX*Nh*og1Yq>p?A9NINsE$CF}>O(Nf~Nz)ZWd$ z&07cuAMOjx1)>b4d#;2=*9O6=y>1f_G?@ZlkwX?lY(Sb{iZ?<=w6Cyyy(m=oF- zVK+vbW^#_Og79D-wI^ITEJq}50*(>ukQA_PgEzQPw?H2vW zGGPZSvrP-3%b0Vs4Rku<%mc_aOon!xh-`_#(vf9OGRJCnQrhTvI4SIwjds&k3NLj% zjzhnmPCMrmuBD6qMMzgM6ajJIKSBP@{;B>1QEWqLW{Sv9EMI*`jT3<)PrG}^CfB-t zgp^;mrn+CiqPdP7bmzYC zg;`VUMIpV*_Y63Nd|B)qy9B)|)z%up@0W%Ruq_4Q&VzPl1g<>LD^eesbOP!YTG`>- zO6TNrF?WtA{+1&!m1@mTwgprd}nGN6%bq?32>al&;?re{EXHIP0o7;O$G2MvAR@0i9?9e2VLrV~p zkcthM)W>?YhM*8*-G_zD$yZudp+atgp%;p6>q$Tqu_$4LmCef?Bdp^C? zl5M7z)H##3&lj}d*!8cN+)kgAXqU3@C#NJ^M1GAG2)icul~lQ9M)L{qJ&hy{_nur;RW)elLm{`MmAK4EbUXohu&N{q zqFoyAd=_GJ?P9mli;y%$43NT`tZ8=D+JT1{FYf!Wbkv*R!{JJPp@;SP%xa;@2MZ`f z*_!LYQADfq#syte&1o$W{d?9?k^MYZmAZ)r*19SOV96O`L4#dmQ^v zeV@7KCwNp7$|D!f3q;kV*g6s&tg87mxkFQkpEC3Y=0fC}cs_>QgbVg7%wLe7ul+<< zL)v}9N1*`B@j^y@z&~SJfya`fjDQx$ z#>~GH_DG-JlAamvLudA&z9O$uG>KRX@5POirt@Pigq~2}Q_Up2--QY_p7`lm5z2EH zPZx9aHftGAFgOQ1#2qIDzbo~Vyhxx1b0_?vkcjAI0IX(1|FcmU zY_~fpcWG6S2c`Swyg%Jt&b*t)^mOl!?ce94F#L||K`qkyKiOcnBD)j(C_{eBt zn)cKguVAG>_h4DaqR5G(s@NAOSlYEwzJ@;iMJR&)lZfKTMMUqa*A^hEeRj3Qi77Ub zqW1~W4#rc1!GE$DniD9Nn(jbE%dEByvDK1v6#lSWNtt6YCzzr20nY>Sc2vCIwsANR zY*dG2U`71|D~@|$)pDgsa_tXWo5GSZIq_(L%Lm_ri8DKu6~ImXoW&jAWabbD7nF&AzIaa zm((2Q1ZorO-kG4nf|7C@Tau_uvyu#ypR8j)(fcg=iu)|6VhJ`0heO_ipkU-N|4<83 z?dgnOCz^wRM?eq|c}!_P?*@Hvd+yQ}Bk^I3JB823{7`9Hy1ahnlOHSZ?Spf(6k zpILi9S)tq~O4gO!J1z%?_$Ub3+fFn%fA#)aV@n=K5ok6Pa zE#w6eT=+b7B3yYHmh?o2rf?t?Q$+ZQEveUM)MpU-_3kofon$G!S`PnqTlw~GE^Q8P z5|aGKFK?5G-lr0pk0^#1xmYMvrFuH4H(=KV+t%ggyZ;c;pBpZW>M6PZJa(UYO1(s4 z7t<_$zf}RzdgDLhEFb6~rz~uIffg?MA`nLDLAx1tw!YS21QF?^U zthP*pB_kyxOYpap2hwi%D_Sdath*AghMgtvQb(tceR+1rRQaC#>=&R%p?fUu#HFcR zBIStmW&Ii9cNg(5Gm}`JB9`lc;5Wz{A@;SYh+=+S0l5C(OP)5cHpn6q>cgYT6I8H&Ary6! z(^j|C%Yz9@bC{lFU&9&{X;V)#RG<4I6v4+BZm8{N!17klJFGM?!HeRWY-}m5nn@qc z5Zv8fdukMQM7b&UQ>=__QOj15F+-EH(u8n*_Tur<8sy@;PQj0_%#x!gfRUAN9nkQE zQb*PR1=nH9e}?o({{$7>ss5Mg5}`V-CvFMp!eV2*JslC+0-+C+FgovC=v-B;_3RS8 z_mLW_Nik&V2x9+MjApIWT%xDWIK?r=G6i^JdsBK-8Wqk;&mzK5i}m^pL~z9_Zkxkn zR)iWayo&tUX$E_NJ_gL7oQUZO6xa)tp7;yL?`gd@vB=5E{^(Jz1OHpOeOu#smrs6F-OC5#dg^1S zj)({%F-HNHUIFwtA#9cok!)1j31pPBaEaqd3fiGuf5{Jk=xM*+dEf0!vPnH_WT{m7 zz2xN01s1MnQl)h3u1Df3`D$Qqyg!Micya_bcM69M{}c4$20px)?z3#wb?_*w(FAKR zgWO%hCt+a7Xa3Cs+{mSi{eHzkS0*#CbEYX0`RtqVfW1e3gYAv{xcm}$TN{aI${?u{ z)>m`VVdkM3=|{aaK}5BfWvkRIDGyG(px!9-eY;@VIE9D*&@1lD@4@fNAAvE+*V$0` z!N4fssdRjtnwc944|wUddEE&QkA(r`xxz;9#JnI|{}5F|GHY_j=|ds{nq8F>1L~oj z?kA>l^SV=9rM#s~j{osJ6y*8=+w6qJQ~&6WZm+B5uiGC>%tK?)7_`73++L>Q#^~`woW!sCZ z+eWGd=?BVs+atwl)g#6YW`Ct;@%VedTeX0v7z91?eCB*9JU-7a$wr_buk2e4sv3{4 zy^Z)$)@hr(Zz6R*?X^dLvDZB(Em>i}7@oz#AM#iOZ*G%QK}LH}2x`F^R0Kh*O6N5m z@XK`nj6wq{p-Y~x$`d(>f;g_=J!pZ2|GtYm!Vn)?^yAa7B-s$Mimqtu+G&+J(uAY+Zgd+%$StMIBTbhnQ8u z=I~M*mHE7qzVpRa?F}OgUCOUr`S-9*LaQe_mA?w|XQz*>f|7G~{^LyWo`7$htKwv# z=Klw_LF?;VPBYv^e5dfvg*>A?dzoe;dTou5`+a4lxB)9WAyMdzfi*i1fSIurF3c%R zu%C!3mJ{n1-irZv@ovr7+2rtMJ}z^$)L2!&1gLVUzvl029n3U%Li(MezFO|ZNyAaw zHkZaW8(G}4U5n`da`XFq(bkxEMPDFe*tHS!J2d~|^tT;W**P6{JRb4Ywm1vB;W4T5 z{$tG{-E2$pwV9=b!}hNBDSd}gkD;n*i{(}7Gdwg)tl&W(GZG|lX@HLkb-M4?MXdU? z373FoPr8UhNKW(cpA}sKQZ`OHaSurt|01)3`jxB?h7+=mogQA*Cs5ZvFz&JoA9Sq{ zcrdI8K|;%Im}XTvQdKBB`8R4$ z=81tVX8NdlmHmV=(*;NAEsz$~fP)`x3T1Sk{jzAYQoE6_HQwTbjWb9^(krk~ znK{@d+>yF*SAV^>=A_>dNygY6I>C9zaV_V-?)Zp^gG0JoSJeD)Cw|oAuisJ?NUevQ zEK}7vt{(g3iVKo=(*B0EHaagff_pH1*nYqPxpu6x^An4aLP;Hm&0QDc6|C!DFoFC3 z3R_*~bivCkN7oP-!fSpZ&3JN2#;6u(V|G+5c2uN7d^WbF#b1grPWuP6HbFcZKLkNB8U*vqWdNHTa~7R(sbT(EkxVva3T^|VKb z6=dw^?}s6w!WPdGk5Kz6?49-i5__BSkU^soL;Y9G%VN`nLmh^fb`^RFSH>-em zR!W>qqCOMWRj4Z*kt%MxyA4GeHgbk2c5>?LA&DxzaLZaC%tHpB8>_s>and)`yUAxu zf9{ozzsAj;@G3=CieoT~b07OhwGKAD-7IhZ8&~?QNHZWYt zHN|`ov$QH=*pehmA@7ysZ-#Y5n>gr@1)XX_@U|>$vDux-)eN5*JMd^UKI3g+ywg}TSw(XiJ#{R-G~k!1 z%>IHqL=I((sScFOahY&sMctxrX_)vHP`L%yR=O$wf%mVOT@hkQidAa57`#(zfH7DK0O9;DnXJnLBz%=I_J{xi}aYD9TVMtz-@I(zED?hh}PXW7oD{nco#?7fqC z{3`1}SfUDpGM_@!Z*NtnXEpi3>aM8DD5F%N=vV$bFN0u|DBq#39V1324ki{RK(RQJ zDr&EW(vXG(8ACkYNHEpDh`zo3_7d2P(iv%`n6qv0H@8a^p0Nt%Yep-7tn|N`CAu#Y$JYf2Aa6uluOOK zm{>eE`kc@SE{2-Yc6mWa&N2D^3C53eZw4@Jxy#>dg@OmU&n()pyROi4@1(_{-XBuF z1v1JN4k5aLN(OyzTu7WVHz=8z4ZhT?ry$gsbN8{@Q-bs2zKayIpXANWfj)|y&7I9t z-0; zl}d8ypLwe7(=aEz?uh6pC}iYk;8A12v_xk4gsP>aMe~?#>Y{jdnbHD%|Lol__k@yx z0d}ko;2GRd@jnF#iz6rI+ct-#hIt+lKe=T}W2VIaBql8@Mg+O63pkQR_B)~WFufwx zQ2h3}I)N<p$xMaAMVdEifXAd{J4z+Yab@xxZKr-L)7QPe>2$DetN8 zVL}KbdiB5Zq>6~9He-wmjFB7aq5kG`Bv&{u(aB-?9o$D|70*4=*Q~$nXxM?sw5(QB zVh|opPY}mKA_c1GWeL`%UO}%RHz2jeD*Ib>AmNN9!nZ3AlM>(&aME^aqRa)9WmB~k z&=TYe<=En1?tu?;-`wgFLe57{2pc!)z}01$Lm6w-6Dvgz`{~6-|1}4DpV!Eew4)f+ zKP<3Sw4aO?t$s&ct7DAC2&|k$qUWpkB0K%v{tvlt2Z?#8XbF6mVQH>6NdI1X@&lY& z1W@xGvp>E?Z|+$?%A;vzftfs|_}NNNNawxjvKfM!X1r^ZFb=D@5yTYV4l>+Vx_?J* zVD?OU39Vn%aj1&OsTS0CYw4em;08>PArxLlc2ho!?!D!;QeS#H6AGZ^Ac9uTpwJgD zwR*JQ*pDcWD32+7n=h7)x|YZpOES#L<|-5_nX9QxP5b-9!V8?{_)oH_&Ky^|>N*m^ z0vy7ZTq$>K$NF79b_1XF(f~=RUmz-Atuoa3ZY`))So5xGp8>(;HJTieo z6iLpP%>UdPD@uUOg>i@Tw{S;CdxylSWp$5}2wuq>pX0j@p5zw32bxpp{b3Q<4pFjM zoryzTJH#p^FzEr=NH%ZFbqb5!%46-Urr=D&k-53owLCs?2`?5et})v%sNf{r@UG9Y zI$9m^{&rd?k{E4LEXLNl-5%kC#jECDy|!8dUTyqV7ShwR!bpVJg;J83xXe!Tr(!*2 zd#fusfD&>ZBA=CCiRjP+T-A0e|nyMB{WL{8+jfjaMX zudv7%;j)tI*mhC8L(*4Yy)S$z#Kl#`oyD7g6gnPDl@~s#(x;MJRAL>Do)YaD4HrGZ(Mc`o2o=DxNx+L4i!uV$ywt(Sx z8@N`y;G60U;Jcy8%G8=HkRjlMEpW??&4uN6YH*^ErTivq76 ztrc#&qR~QlkAw(sKf9@kq*P_4O;a1Ofox~;D;y^3=K%8iKA#nz)f;4u+N@d%B%98= zt~1#3bbPu=B+DB>p*U6RBqOIXv97FDVo%~B<85F1w(zkb(wxaL%U`TvHS?x-{378b zsdtn;9YO6A)Xu>^0TZ~;PlyRawB8EhT&X6!IL@kOc`!`IsA&wLURnlpgQ4|*J1v(G zx6LDPFl(A?&TC={mb8q&l!kDP9a--M5z{TDcf*-?bA96PmFLnUHV$n!GOP+fzOcQ( z2ip~3s*aT3_O7k#&-(&W6zB}d(Qc@UfA|scKzH0dqIWTmqAilq5a~`eAT5C{U!O4a zMak37lOU1fkDx2Q^=T7j^AeRkw9$`Q7hq3rqXa+iv}%L$%=pH3&UWQ~-+ZG9P{|xQ zL;+|Q%^!4PTKL``bfJgN(IcWD3KzacmCiMy=3TP%98HIy$?#tg8hUhj4y5r`mTlGJ z`O#FfQ+D(+C$sVJ@ryo7>X-JM*yYZ$LFB&_RjS0?>h|xZE+s|1&6yqR|E2BJ(>wT0 zMisg%d@cf3LvGv4Gj72fL_OxcR7a*Hd5)h%n1%~f5~MhD(qU^Li1kOuPJoHbWDh_& z`ADbjh39AUZWAV6FnOJw3D$zzuKo#l~2|+^Z zyUspOoWWt9Zbx*Z(P6XYPJmfeek;;NmF%uFt_yul<4KqVdD_JYB0R_}S#NVYJKWgw z<_9~(Sma+KJ}N)&PBc?3;IpI6Qcd6mPyy~Z;+|QN^rQ3+*x;w7AY^X;t$=mYZBs%1 zAm4!rSZdkdKgHm$YsWnMEw4l$ls=w@`iI{~dk}O2-rOfYCgKj^ zzbRo4qmn~_(CG0jal-2_4<-CrxSjoHWP2xJtp@|pHB^&(W8BuCK;3D zne^7E`Se`@9@5*$bx{)#=_^0J;=BL%QZwG#61Z}EM@##+ebQxGbQzpuIfgbaqyiU} zYGzpsfVR9M*bq8z@4k=uAZOSUr38RFO77DfLrH4xl2${XusqO0`;a;RBMGIG^QYNF znAR}XK9Ce|aENrqUC;O7&%O*T&V2svj4?rKiJ8#1VpGKr2)Sob{LYD!qq2d--}_$4 z8!im$+^#Wu$AlesI3L_JA>00M+r%MNJwj_H(kkZUgK8qsUk|=(Fku{`E`BJc+WQ>h! zlmM192kNYr$HZcS9FN+5g$MJZZ_PgFR6p4hVfBCz) zDoz~yxjn=JjQAlXZVD@UhI8>pTMKN2FQWr*@CdPL26^E}u2nf!BB5W`%^FA$=CbDq z1+gatsFmN*8Aq6gLLTGqR6F6bw1t`05oOdiacCtfXFTgM_nhd^y2N`-_hw0=Ok%Hy zi;%C`T?!Dbv!C!%a>oL0@n))?KlO_X(mG(j1Z@s8a@)c4{+)2cRu2ojVtCA1HFu*e zAu0Tfdq8Wg-GpLEpnAb%{Jj!d=g3THkO?G_)FnpggXuvXMHX%ttdwP^;klr|ZDXr$ z)Uhx7BHJP}%*`g!i2-RRXi~vn?ml*ewtPA@&x5)D*)A)9`7RB?b_0}cRl4-s75G?)NwZ-H^{EL!fO5 zBu(#J7Z`}xq5owU6&xH9E$VL*RtcPRMq7s{G2--OIM3a;t!SmF2I)V^hyWkl;(e<_aGCyJALzinSPq~7R{-hCY%4{ ztGcPdr8d2vSA0H8l0M4(_Gg*@<41PPfP+@)xMQZt^iRVd9HgW}>W827yAG1oYnZK^ z#^Z6i7DPW-{jq6_$i5XOG_J*LQclU7x;7g8f;BH;6pYQh(5fa9p`I%+R6QH@MejRu zad(qa&W^uV?8A=mYKT1gA&n?(Z}#SZ2Mil$bPaXX&sn4W0;Gg81&v?iNTE;a279dd zbJPf5h%XPfdLv5ll|-*30)0Z5kfpJm_6!z4a?cq%I8pMV!!N4gfd%oFw8&C3x%vG; z$}wC>mXE(!crr{zwp@r8e@BSTNH$4%9wEzO)WDRBgpV9sr4v>U>h`?ARme!I74!BkgiWm%vOvo|#MWOn^hor7L)4&z#%zgq&;wd{D zBZx~Gx5o>44S+Vw2H8LV(!*wV5j-e}XssaoHt1heX6rW)zOGLpPUBcNBtMK}e+Z{d zM|e%oyO9~it7`9wKpG9lIDK8@_=}<NOQPr z$L5TxmdC5Wi3A_%aV1RAF(RKLet(T0C()6`XWakR*Jb%jl-MC^|&OAeI zU4l!p58H>YM>_Kyy{3O&4q-S9t8-{g{Okt?G5L=#v z`!VVE`-ktN5M#oyT3U4qyzT&r8_%1xo50G-WTRKg(P$x0vY7_Uyy?1B6!BTvZYbF} ze^+MwtQX15kqH*Xbe9ORo@_&Y~tj6Z?E(-E$K zRK*Wzk&HOAz%6#J_<_Oc9fXh9M4Hs7Ut*kR=kEnEcZJ^@bkWn)BBHEGBAlDk{+?nh zCx58UJ;fQJPN`W0xd~N2&xmLY{CCiPP?)ouk}t#Y9=p_=81|2weCzuv^h(l*f#%(x#$iy=^ep|BnBz zNpKLwh1CIB-*Cn7ICSalGp1^P4a&1EUz)PboccJRSTa_grmRO)uO~gmAC6^5RuM@$ z?i|^(GtC;D!aRJ1ZdH+0dUGd{rmt7RH`1nX@=-&X(^INlfa+P$Gwb5kyd|@N9Ips5GG*LRtqVrgtiHr6{wzNou@Z?zn7OV2(ZNupiOR#R4E z$rAB{K}(=F^oclCx7|jw-8qlbuXtk=95vrCgu^wM!7^NC&#RvQb#KnzUJH*+N;T}F z>>GxeSe{JbfxcPwdyUIj-Jg%N3w}DRb2ZU+@yzYzT9u6@Bq$Bh#W_wW5~36_R#WKR zMJ->#+EnWZ%@eB<(_`V(BPeJNYzK0xd9oEWE;C>L7-}f`e=RD94akUf_N9JsLG5IF z8!FxTZl6MHk!4Vhwcz+&vVLk`iD<#nW`4|qf?!^yVZ{|0To()Abqp3WR80^3GwZJu z{PATB_k}^qYrcx(CHk^STjiuj<0@IbEp^oT7 z9vpW)&eqt$`j5+b4YtU!azWoHTrzR7b$4s)IE0*AU?})U_)ZDWS97jZFIgH2o0A{r z|1E99yNvT7#<;*GI}?7gmJ6=gh?4uL?L#KP+~D0LLZHr>a^Q6_{Bf|-Oq&x7LfYoo zEbYJ_CpEDOjgija&Wk=O{Cv+S!@1epW`)X>!y4VWWD37wvTbi-EJ|IIy;%~;qoX8+ z+4DzEI&Hg1WKT7)(O?*XRrag7aFq;dGk9u5?aZ{R98nDPsmOO}o~ywnsA>=90k6Xg z?M?h1IAuS=`uQNF$)S3QEXc6QuthbPtUpz+(EgL}aXZky zJI*L^#ach;x-*RMsWV*tTk`aZn_#*aJ`o#jIU1X4=$^M|xuu=1{jV0Yq#Wz$0T&~g z4=Oz}Rtg7!-lf#A$%1oB5rL|gs?~CMsb2&lc^0Jx$VpDtF$2eTQe`#I59X@nUd*>o zH(wxxO>I`wkGML<%eRzEY+{9CGs8H|f>Y^zP#-3X-78gC|#f z22Q11g_Us)1)ya^z$Jf0hwQpC) zc=cjTDDSm7o;Hcf;TD|JL*yk(HK-hqq{4F7$VAm`^gw(lwGMA^!vPa|i7J literal 0 HcmV?d00001 diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index d526909e..3e2faad1 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -28,6 +28,7 @@ class $AssetsImagesGen { $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); $AssetsImagesLaunchRampGen get launchRamp => const $AssetsImagesLaunchRampGen(); + $AssetsImagesMultiballGen get multiball => const $AssetsImagesMultiballGen(); $AssetsImagesMultiplierGen get multiplier => const $AssetsImagesMultiplierGen(); $AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen(); @@ -180,6 +181,18 @@ class $AssetsImagesLaunchRampGen { const AssetGenImage('assets/images/launch_ramp/ramp.png'); } +class $AssetsImagesMultiballGen { + const $AssetsImagesMultiballGen(); + + /// File path: assets/images/multiball/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/multiball/dimmed.png'); + + /// File path: assets/images/multiball/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/multiball/lit.png'); +} + class $AssetsImagesMultiplierGen { const $AssetsImagesMultiplierGen(); diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 394f32ed..5b661691 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -21,6 +21,7 @@ export 'kicker/kicker.dart'; export 'launch_ramp.dart'; export 'layer.dart'; export 'layer_sensor.dart'; +export 'multiball/multiball.dart'; export 'multiplier/multiplier.dart'; export 'plunger.dart'; export 'rocket.dart'; diff --git a/packages/pinball_components/lib/src/components/multiball/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/multiball/behaviors/behaviors.dart new file mode 100644 index 00000000..052b4a4e --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'multiball_blinking_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/multiball/behaviors/multiball_blinking_behavior.dart b/packages/pinball_components/lib/src/components/multiball/behaviors/multiball_blinking_behavior.dart new file mode 100644 index 00000000..48c90552 --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/behaviors/multiball_blinking_behavior.dart @@ -0,0 +1,78 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template multiball_blinking_behavior} +/// Makes a [Multiball] blink back to [MultiballLightState.lit] when +/// [MultiballLightState.dimmed]. +/// {@endtemplate} +class MultiballBlinkingBehavior extends TimerComponent + with ParentIsA { + /// {@macro multiball_blinking_behavior} + MultiballBlinkingBehavior() : super(period: 0.1); + + final _maxBlinks = 10; + + int _blinksCounter = 0; + + bool _isAnimating = false; + + void _onNewState(MultiballState state) { + final animationEnabled = + state.animationState == MultiballAnimationState.blinking; + final canBlink = _blinksCounter < _maxBlinks; + + if (animationEnabled && canBlink) { + _start(); + } else { + _stop(); + } + } + + void _start() { + if (!_isAnimating) { + _isAnimating = true; + timer + ..reset() + ..start(); + _animate(); + } + } + + void _animate() { + parent.bloc.onBlink(); + _blinksCounter++; + } + + void _stop() { + if (_isAnimating) { + _isAnimating = false; + timer.stop(); + _blinksCounter = 0; + parent.bloc.onStop(); + } + } + + @override + Future onLoad() async { + await super.onLoad(); + parent.bloc.stream.listen(_onNewState); + } + + @override + void onTick() { + super.onTick(); + if (!_isAnimating) { + timer.stop(); + } else { + if (_blinksCounter < _maxBlinks) { + _animate(); + timer + ..reset() + ..start(); + } else { + timer.stop(); + } + } + } +} diff --git a/packages/pinball_components/lib/src/components/multiball/cubit/multiball_cubit.dart b/packages/pinball_components/lib/src/components/multiball/cubit/multiball_cubit.dart new file mode 100644 index 00000000..9d943c9d --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/cubit/multiball_cubit.dart @@ -0,0 +1,37 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'multiball_state.dart'; + +class MultiballCubit extends Cubit { + MultiballCubit() : super(const MultiballState.initial()); + + void onAnimate() { + emit( + state.copyWith(animationState: MultiballAnimationState.blinking), + ); + } + + void onStop() { + emit( + state.copyWith(animationState: MultiballAnimationState.idle), + ); + } + + void onBlink() { + switch (state.lightState) { + case MultiballLightState.lit: + emit( + state.copyWith(lightState: MultiballLightState.dimmed), + ); + break; + case MultiballLightState.dimmed: + emit( + state.copyWith(lightState: MultiballLightState.lit), + ); + break; + } + } +} diff --git a/packages/pinball_components/lib/src/components/multiball/cubit/multiball_state.dart b/packages/pinball_components/lib/src/components/multiball/cubit/multiball_state.dart new file mode 100644 index 00000000..bbc66fd5 --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/cubit/multiball_state.dart @@ -0,0 +1,44 @@ +// ignore_for_file: comment_references, public_member_api_docs + +part of 'multiball_cubit.dart'; + +/// Indicates the different sprite states for [MultiballSpriteGroupComponent]. +enum MultiballLightState { + lit, + dimmed, +} + +// Indicates if the blinking animation is running. +enum MultiballAnimationState { + idle, + blinking, +} + +class MultiballState extends Equatable { + const MultiballState({ + required this.lightState, + required this.animationState, + }); + + const MultiballState.initial() + : this( + lightState: MultiballLightState.dimmed, + animationState: MultiballAnimationState.idle, + ); + + final MultiballLightState lightState; + final MultiballAnimationState animationState; + + MultiballState copyWith({ + MultiballLightState? lightState, + MultiballAnimationState? animationState, + }) { + return MultiballState( + lightState: lightState ?? this.lightState, + animationState: animationState ?? this.animationState, + ); + } + + @override + List get props => [lightState, animationState]; +} diff --git a/packages/pinball_components/lib/src/components/multiball/multiball.dart b/packages/pinball_components/lib/src/components/multiball/multiball.dart new file mode 100644 index 00000000..ca348604 --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/multiball.dart @@ -0,0 +1,138 @@ +import 'dart:math' as math; +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/gen/assets.gen.dart'; +import 'package:pinball_components/src/components/multiball/behaviors/behaviors.dart'; +import 'package:pinball_components/src/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'cubit/multiball_cubit.dart'; + +/// {@template multiball} +/// A [Component] for the multiball lighting decals on the board. +/// {@endtemplate} +class Multiball extends Component { + /// {@macro multiball} + Multiball._({ + required Vector2 position, + double rotation = 0, + Iterable? children, + required this.bloc, + }) : super( + children: [ + MultiballBlinkingBehavior(), + MultiballSpriteGroupComponent( + position: position, + litAssetPath: Assets.images.multiball.lit.keyName, + dimmedAssetPath: Assets.images.multiball.dimmed.keyName, + rotation: rotation, + state: bloc.state.lightState, + ), + ...?children, + ], + ); + + /// {@macro multiball} + Multiball.a({ + Iterable? children, + }) : this._( + position: Vector2(-23, 7.5), + rotation: -24 * math.pi / 180, + bloc: MultiballCubit(), + children: children, + ); + + /// {@macro multiball} + Multiball.b({ + Iterable? children, + }) : this._( + position: Vector2(-7.2, -6.2), + rotation: -5 * math.pi / 180, + bloc: MultiballCubit(), + children: children, + ); + + /// {@macro multiball} + Multiball.c({ + Iterable? children, + }) : this._( + position: Vector2(-0.7, -9.3), + rotation: 2.7 * math.pi / 180, + bloc: MultiballCubit(), + children: children, + ); + + /// {@macro multiball} + Multiball.d({ + Iterable? children, + }) : this._( + position: Vector2(15, 7), + rotation: 24 * math.pi / 180, + bloc: MultiballCubit(), + children: children, + ); + + /// Creates an [Multiball] without any children. + /// + /// This can be used for testing [Multiball]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + Multiball.test({ + required this.bloc, + }); + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final MultiballCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } +} + +/// {@template multiball_sprite_group_component} +/// A [SpriteGroupComponent] for the multiball over the board. +/// {@endtemplate} +@visibleForTesting +class MultiballSpriteGroupComponent + extends SpriteGroupComponent + with HasGameRef, ParentIsA { + /// {@macro multiball_sprite_group_component} + MultiballSpriteGroupComponent({ + required Vector2 position, + required String litAssetPath, + required String dimmedAssetPath, + required double rotation, + required MultiballLightState state, + }) : _litAssetPath = litAssetPath, + _dimmedAssetPath = dimmedAssetPath, + super( + anchor: Anchor.center, + position: position, + angle: rotation, + current: state, + ); + + final String _litAssetPath; + final String _dimmedAssetPath; + + @override + Future onLoad() async { + await super.onLoad(); + parent.bloc.stream.listen((state) => current = state.lightState); + + final sprites = { + MultiballLightState.lit: Sprite( + gameRef.images.fromCache(_litAssetPath), + ), + MultiballLightState.dimmed: + Sprite(gameRef.images.fromCache(_dimmedAssetPath)), + }; + this.sprites = sprites; + size = sprites[current]!.originalSize / 10; + } +} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index 1d2232e0..61e62386 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -81,6 +81,7 @@ flutter: - assets/images/google_word/letter5/ - assets/images/google_word/letter6/ - assets/images/signpost/ + - assets/images/multiball/ - assets/images/multiplier/x2/ - assets/images/multiplier/x3/ - assets/images/multiplier/x4/ diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 25473f02..9fdee65a 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -27,6 +27,7 @@ void main() { addScoreStories(dashbook); addBackboardStories(dashbook); addDinoWallStories(dashbook); + addMultiballStories(dashbook); addMultipliersStories(dashbook); runApp(dashbook); diff --git a/packages/pinball_components/sandbox/lib/stories/multiball/multiball_game.dart b/packages/pinball_components/sandbox/lib/stories/multiball/multiball_game.dart new file mode 100644 index 00000000..83b53785 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/multiball/multiball_game.dart @@ -0,0 +1,56 @@ +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class MultiballGame extends BallGame with KeyboardEvents { + MultiballGame() + : super( + imagesFileNames: [ + Assets.images.multiball.lit.keyName, + Assets.images.multiball.dimmed.keyName, + ], + ); + + static const description = ''' + Shows how the Multiball are rendered. + + - Tap anywhere on the screen to spawn a ball into the game. + - Press space bar to animate multiballs. +'''; + + final List multiballs = [ + Multiball.a(), + Multiball.b(), + Multiball.c(), + Multiball.d(), + ]; + + @override + Future onLoad() async { + await super.onLoad(); + + camera.followVector2(Vector2.zero()); + + await addAll(multiballs); + await traceAllBodies(); + } + + @override + KeyEventResult onKeyEvent( + RawKeyEvent event, + Set keysPressed, + ) { + if (event is RawKeyDownEvent && + event.logicalKey == LogicalKeyboardKey.space) { + for (final multiball in multiballs) { + multiball.bloc.onBlink(); + } + + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/multiball/stories.dart b/packages/pinball_components/sandbox/lib/stories/multiball/stories.dart new file mode 100644 index 00000000..6993ed92 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/multiball/stories.dart @@ -0,0 +1,11 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/multiball/multiball_game.dart'; + +void addMultiballStories(Dashbook dashbook) { + dashbook.storiesOf('Multiball').addGame( + title: 'Assets', + description: MultiballGame.description, + gameBuilder: (_) => MultiballGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 89bf5d96..8cdd38b1 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -10,6 +10,7 @@ export 'flutter_forest/stories.dart'; export 'google_word/stories.dart'; export 'launch_ramp/stories.dart'; export 'layer/stories.dart'; +export 'multiball/stories.dart'; export 'multipliers/stories.dart'; export 'plunger/stories.dart'; export 'score/stories.dart'; diff --git a/packages/pinball_components/test/helpers/mocks.dart b/packages/pinball_components/test/helpers/mocks.dart index ab867e3b..99959e03 100644 --- a/packages/pinball_components/test/helpers/mocks.dart +++ b/packages/pinball_components/test/helpers/mocks.dart @@ -25,6 +25,8 @@ class MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {} class MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {} +class MockMultiballCubit extends Mock implements MultiballCubit {} + class MockMultiplierCubit extends Mock implements MultiplierCubit {} class MockChromeDinoCubit extends Mock implements ChromeDinoCubit {} diff --git a/packages/pinball_components/test/src/components/multiball/behaviors/multiball_blinking_behavior_test.dart b/packages/pinball_components/test/src/components/multiball/behaviors/multiball_blinking_behavior_test.dart new file mode 100644 index 00000000..2b3885f9 --- /dev/null +++ b/packages/pinball_components/test/src/components/multiball/behaviors/multiball_blinking_behavior_test.dart @@ -0,0 +1,158 @@ +// ignore_for_file: prefer_const_constructors, cascade_invocations + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/multiball/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'MultiballBlinkingBehavior', + () { + flameTester.testGameWidget( + 'calls onBlink every 0.1 seconds when animation state is animated', + setUp: (game, tester) async { + final behavior = MultiballBlinkingBehavior(); + final bloc = MockMultiballCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: MultiballState.initial(), + ); + + final multiball = Multiball.test(bloc: bloc); + await multiball.add(behavior); + await game.ensureAdd(multiball); + + streamController.add( + MultiballState( + animationState: MultiballAnimationState.blinking, + lightState: MultiballLightState.lit, + ), + ); + await tester.pump(); + game.update(0); + + verify(bloc.onBlink).called(1); + + await tester.pump(); + game.update(0.1); + + await streamController.close(); + verify(bloc.onBlink).called(1); + }, + ); + + flameTester.testGameWidget( + 'calls onStop when animation state is stopped', + setUp: (game, tester) async { + final behavior = MultiballBlinkingBehavior(); + final bloc = MockMultiballCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: MultiballState.initial(), + ); + when(bloc.onBlink).thenAnswer((_) async {}); + + final multiball = Multiball.test(bloc: bloc); + await multiball.add(behavior); + await game.ensureAdd(multiball); + + streamController.add( + MultiballState( + animationState: MultiballAnimationState.blinking, + lightState: MultiballLightState.lit, + ), + ); + await tester.pump(); + + streamController.add( + MultiballState( + animationState: MultiballAnimationState.idle, + lightState: MultiballLightState.lit, + ), + ); + + await streamController.close(); + verify(bloc.onStop).called(1); + }, + ); + + flameTester.testGameWidget( + 'onTick stops when there is no animation', + setUp: (game, tester) async { + final behavior = MultiballBlinkingBehavior(); + final bloc = MockMultiballCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: MultiballState.initial(), + ); + when(bloc.onBlink).thenAnswer((_) async {}); + + final multiball = Multiball.test(bloc: bloc); + await multiball.add(behavior); + await game.ensureAdd(multiball); + + streamController.add( + MultiballState( + animationState: MultiballAnimationState.idle, + lightState: MultiballLightState.lit, + ), + ); + await tester.pump(); + + behavior.onTick(); + + expect(behavior.timer.isRunning(), false); + }, + ); + + flameTester.testGameWidget( + 'onTick stops after 10 blinks repetitions', + setUp: (game, tester) async { + final behavior = MultiballBlinkingBehavior(); + final bloc = MockMultiballCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: MultiballState.initial(), + ); + when(bloc.onBlink).thenAnswer((_) async {}); + + final multiball = Multiball.test(bloc: bloc); + await multiball.add(behavior); + await game.ensureAdd(multiball); + + streamController.add( + MultiballState( + animationState: MultiballAnimationState.blinking, + lightState: MultiballLightState.dimmed, + ), + ); + await tester.pump(); + + for (var i = 0; i < 10; i++) { + behavior.onTick(); + } + + expect(behavior.timer.isRunning(), false); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/multiball/cubit/multiball_cubit_test.dart b/packages/pinball_components/test/src/components/multiball/cubit/multiball_cubit_test.dart new file mode 100644 index 00000000..2fcb5ccc --- /dev/null +++ b/packages/pinball_components/test/src/components/multiball/cubit/multiball_cubit_test.dart @@ -0,0 +1,67 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group( + 'MultiballCubit', + () { + blocTest( + 'onAnimate emits animationState [animate]', + build: MultiballCubit.new, + act: (bloc) => bloc.onAnimate(), + expect: () => [ + isA() + ..having( + (state) => state.animationState, + 'animationState', + MultiballAnimationState.blinking, + ) + ], + ); + + blocTest( + 'onStop emits animationState [stopped]', + build: MultiballCubit.new, + act: (bloc) => bloc.onStop(), + expect: () => [ + isA() + ..having( + (state) => state.animationState, + 'animationState', + MultiballAnimationState.idle, + ) + ], + ); + + blocTest( + 'onBlink emits lightState [lit, dimmed, lit]', + build: MultiballCubit.new, + act: (bloc) => bloc + ..onBlink() + ..onBlink() + ..onBlink(), + expect: () => [ + isA() + ..having( + (state) => state.lightState, + 'lightState', + MultiballLightState.lit, + ), + isA() + ..having( + (state) => state.lightState, + 'lightState', + MultiballLightState.dimmed, + ), + isA() + ..having( + (state) => state.lightState, + 'lightState', + MultiballLightState.lit, + ) + ], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/multiball/cubit/multiball_state_test.dart b/packages/pinball_components/test/src/components/multiball/cubit/multiball_state_test.dart new file mode 100644 index 00000000..69789be9 --- /dev/null +++ b/packages/pinball_components/test/src/components/multiball/cubit/multiball_state_test.dart @@ -0,0 +1,76 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/src/pinball_components.dart'; + +void main() { + group('MultiballState', () { + test('supports value equality', () { + expect( + MultiballState( + animationState: MultiballAnimationState.idle, + lightState: MultiballLightState.dimmed, + ), + equals( + MultiballState( + animationState: MultiballAnimationState.idle, + lightState: MultiballLightState.dimmed, + ), + ), + ); + }); + + group('constructor', () { + test('can be instantiated', () { + expect( + MultiballState( + animationState: MultiballAnimationState.idle, + lightState: MultiballLightState.dimmed, + ), + isNotNull, + ); + }); + }); + + group('copyWith', () { + test( + 'copies correctly ' + 'when no argument specified', + () { + final multiballState = MultiballState( + animationState: MultiballAnimationState.idle, + lightState: MultiballLightState.dimmed, + ); + expect( + multiballState.copyWith(), + equals(multiballState), + ); + }, + ); + + test( + 'copies correctly ' + 'when all arguments specified', + () { + final multiballState = MultiballState( + animationState: MultiballAnimationState.idle, + lightState: MultiballLightState.dimmed, + ); + final otherMultiballState = MultiballState( + animationState: MultiballAnimationState.blinking, + lightState: MultiballLightState.lit, + ); + expect(multiballState, isNot(equals(otherMultiballState))); + + expect( + multiballState.copyWith( + animationState: MultiballAnimationState.blinking, + lightState: MultiballLightState.lit, + ), + equals(otherMultiballState), + ); + }, + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/multiball/multiball_test.dart b/packages/pinball_components/test/src/components/multiball/multiball_test.dart new file mode 100644 index 00000000..9b1e0e2f --- /dev/null +++ b/packages/pinball_components/test/src/components/multiball/multiball_test.dart @@ -0,0 +1,90 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/multiball/behaviors/behaviors.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.multiball.lit.keyName, + Assets.images.multiball.dimmed.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + group('Multiball', () { + group('loads correctly', () { + flameTester.test('"a"', (game) async { + final multiball = Multiball.a(); + await game.ensureAdd(multiball); + + expect(game.contains(multiball), isTrue); + }); + + flameTester.test('"b"', (game) async { + final multiball = Multiball.b(); + await game.ensureAdd(multiball); + expect(game.contains(multiball), isTrue); + }); + + flameTester.test('"c"', (game) async { + final multiball = Multiball.c(); + await game.ensureAdd(multiball); + + expect(game.contains(multiball), isTrue); + }); + + flameTester.test('"d"', (game) async { + final multiball = Multiball.d(); + await game.ensureAdd(multiball); + expect(game.contains(multiball), isTrue); + }); + }); + + flameTester.test( + 'closes bloc when removed', + (game) async { + final bloc = MockMultiballCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: MultiballLightState.dimmed, + ); + when(bloc.close).thenAnswer((_) async {}); + final multiball = Multiball.test(bloc: bloc); + + await game.ensureAdd(multiball); + game.remove(multiball); + await game.ready(); + + verify(bloc.close).called(1); + }, + ); + + group('adds', () { + flameTester.test('new children', (game) async { + final component = Component(); + final multiball = Multiball.a( + children: [component], + ); + await game.ensureAdd(multiball); + expect(multiball.children, contains(component)); + }); + + flameTester.test('a MultiballBlinkingBehavior', (game) async { + final multiball = Multiball.a(); + await game.ensureAdd(multiball); + expect( + multiball.children.whereType().single, + isNotNull, + ); + }); + }); + }); +} diff --git a/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart b/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart new file mode 100644 index 00000000..00049a83 --- /dev/null +++ b/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart @@ -0,0 +1,136 @@ +// ignore_for_file: cascade_invocations, prefer_const_constructors + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/components/multiballs/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.multiball.lit.keyName, + Assets.images.multiball.dimmed.keyName, + ]; + + group('MultiballsBehavior', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + assets: assets, + ); + + group('listenWhen', () { + test( + 'is true when the bonusHistory has changed ' + 'with a new GameBonus.dashNest', () { + final previous = GameState.initial(); + final state = previous.copyWith( + bonusHistory: [GameBonus.dashNest], + ); + + expect( + MultiballsBehavior().listenWhen(previous, state), + isTrue, + ); + }); + + test( + 'is false when the bonusHistory has changed ' + 'with a bonus different than GameBonus.dashNest', () { + final previous = + GameState.initial().copyWith(bonusHistory: [GameBonus.dashNest]); + final state = previous.copyWith( + bonusHistory: [...previous.bonusHistory, GameBonus.androidSpaceship], + ); + + expect( + MultiballsBehavior().listenWhen(previous, state), + isFalse, + ); + }); + + test('is false when the bonusHistory state is the same', () { + final previous = GameState.initial(); + final state = GameState( + score: 10, + multiplier: 1, + rounds: 0, + bonusHistory: const [], + ); + + expect( + MultiballsBehavior().listenWhen(previous, state), + isFalse, + ); + }); + }); + + group('onNewState', () { + flameBlocTester.testGameWidget( + "calls 'onAnimate' once for every multiball", + setUp: (game, tester) async { + final behavior = MultiballsBehavior(); + final parent = Multiballs.test(); + final multiballCubit = MockMultiballCubit(); + final otherMultiballCubit = MockMultiballCubit(); + final multiballs = [ + Multiball.test( + bloc: multiballCubit, + ), + Multiball.test( + bloc: otherMultiballCubit, + ), + ]; + + whenListen( + multiballCubit, + const Stream.empty(), + initialState: MultiballState.initial(), + ); + when(multiballCubit.onAnimate).thenAnswer((_) async {}); + + whenListen( + otherMultiballCubit, + const Stream.empty(), + initialState: MultiballState.initial(), + ); + when(otherMultiballCubit.onAnimate).thenAnswer((_) async {}); + + await parent.addAll(multiballs); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + await tester.pump(); + + behavior.onNewState( + GameState.initial().copyWith(bonusHistory: [GameBonus.dashNest]), + ); + + for (final multiball in multiballs) { + verify( + multiball.bloc.onAnimate, + ).called(1); + } + }, + ); + }); + }); +} diff --git a/test/game/components/multiballs/multiballs_test.dart b/test/game/components/multiballs/multiballs_test.dart new file mode 100644 index 00000000..c1a328b1 --- /dev/null +++ b/test/game/components/multiballs/multiballs_test.dart @@ -0,0 +1,54 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.multiball.lit.keyName, + Assets.images.multiball.dimmed.keyName, + ]; + late GameBloc gameBloc; + + setUp(() { + gameBloc = GameBloc(); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + assets: assets, + ); + + group('Multiballs', () { + flameBlocTester.testGameWidget( + 'loads correctly', + setUp: (game, tester) async { + final multiballs = Multiballs(); + await game.ensureAdd(multiballs); + + expect(game.contains(multiballs), isTrue); + }, + ); + + group('loads', () { + flameBlocTester.testGameWidget( + 'four Multiball', + setUp: (game, tester) async { + final multiballs = Multiballs(); + await game.ensureAdd(multiballs); + + expect( + multiballs.descendants().whereType().length, + equals(4), + ); + }, + ); + }); + }); +} diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index c2357046..8d76e8f0 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -64,6 +64,8 @@ void main() { Assets.images.launchRamp.ramp.keyName, Assets.images.launchRamp.foregroundRailing.keyName, Assets.images.launchRamp.backgroundRailing.keyName, + Assets.images.multiball.lit.keyName, + Assets.images.multiball.dimmed.keyName, Assets.images.multiplier.x2.lit.keyName, Assets.images.multiplier.x2.dimmed.keyName, Assets.images.multiplier.x3.lit.keyName, @@ -178,6 +180,18 @@ void main() { ); }); + flameBlocTester.test( + 'has only one Multiballs', + (game) async { + await game.ready(); + + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + flameBlocTester.test( 'one GoogleWord', (game) async { diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 1d3ad3c7..7a5862e7 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -95,9 +95,7 @@ class MockAndroidBumper extends Mock implements AndroidBumper {} class MockSparkyBumper extends Mock implements SparkyBumper {} -class MockMultiplier extends Mock implements Multiplier {} - -class MockMultipliersGroup extends Mock implements Multipliers {} +class MockMultiballCubit extends Mock implements MultiballCubit {} class MockMultiplierCubit extends Mock implements MultiplierCubit {} From 2df41dd0584ce98f4d69319cb53fd6667192e574 Mon Sep 17 00:00:00 2001 From: Elizabeth Gaston Date: Mon, 2 May 2022 10:09:24 -0500 Subject: [PATCH 2/4] chore: Update metadata image (#290) * Update index.html * feat: update sharing image url and rules * updating favicons and organzing index file Co-authored-by: Tom Arra --- storage.rules | 9 +++++++++ web/icons/Icon-192.png | Bin 17644 -> 5292 bytes web/icons/Icon-512.png | Bin 32868 -> 8252 bytes web/icons/favicon.png | Bin 1091 -> 917 bytes web/index.html | 15 +++++---------- 5 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 storage.rules diff --git a/storage.rules b/storage.rules new file mode 100644 index 00000000..03ab51c6 --- /dev/null +++ b/storage.rules @@ -0,0 +1,9 @@ +rules_version = '2'; +service firebase.storage { + match /b/{bucket}/o { + match /{folder}/{imageId} { + allow read: if imageId.matches(".*\\.png") || imageId.matches(".*\\.jpg"); + allow write: if false; + } + } +} \ No newline at end of file diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png index 69c31fc5d462286102af408cc9ee0d9b40eae356..b749bfef07473333cf1dd31e9eed89862a5d52aa 100644 GIT binary patch literal 5292 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 literal 17644 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 literal 32868 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 diff --git a/web/icons/favicon.png b/web/icons/favicon.png index 66a69cb1850861f675e7ae3e8a650aca116a62a1..8aaa46ac1ae21512746f852a42ba87e4165dfdd1 100644 GIT binary patch delta 538 zcmV+#0_FX~2$ctrF%k)INLh0L04^f{04^f|c%?sfk<(NIH7+!>Dgg=se@wzBSpWb5 z)k#D_R5%fRQ$0@`K@fd=cRqteVhX~CfFejq;UFXnBTA^KagFHm0}xFrnxu`CCaJ;? zNSQiC0yYRokkGh5vJg_F*m7YkLUG96z0J&>*Iu6!Z*jXjyYIc3w{t?%z=&a=3Nd`q z8xHj&3{HZQvHuc;1a?UAf1;uA^EL$Ur_#Xo(-b^A6D}b+R2WCfHL2H*1MPhcbm8Pd z*X~tx5O&XbBly!#8j%t!m+K1q)ebVWc}~i&SA1mBqIYgEj#=<*(*ACN7gNB&at!W- zaalH|mw_>d!F!}UNy_sCewXv{$hV8O;bbzGV8Er{60fdh1fC^(f8VeAkfd5^Dx_Tj zNyX-#IPht0#y|;d#+d>*?GJe$ZCbzl(Z<%5M849AN@TF@TCW7dSguPvn{?q)V6zxt zdfY+AcX6;Jurlp|Mf&>P5)9aw_&~Y7_!8hwxM(Vcqil$cr3rLYh?F~!GzND3d`vP% zopPA<(QHfVq*HwQbod#qKS2bh9x!_G9GjTqvQS&x?N&#JR;pwqYEnE{=%Josk3vU`Ryr c*Q5)70RS={HmF&bi~s-t07*qoM6N<$f+UXhivR!s delta 713 zcmbQrewbr|VSP(#glC$sFM}2X0|N&GE29ttGmymygba*Q46I-_1A`Z%G@Kp8r~y^O z#K6#=$-n|t69uF}zypYZx}h}l0!D<13z*=l1r{)a*vep~0w8_I^JP8)Db50q$YKTt zZeb8+WSBKaVxn^hvzea3##vm9^^$Q9*@4D?_H=O!u@GDuyt6ASP+-mLE~Bm;j~47m z;ZtjpUe>hi-|YVjIhqoZG88zPG}x?G7&LJ=vAsLDxyr)mvqan{sXg;f&tD+s0;9(Q&bO~4)5>UVXx?uOwc`9D5jYfXUg}+qo-H>l4 zt+?pR!-9^-A3J>1l;t+`#hrOxyl3COQxc07+PrYU+a4f?d6%W~TF*HbSZd%RJ?+v)AcA5U!Foa@ao>pqy! za5AN7`gHLlkLwF}+|4tbX*&HhtKEG5Mu!L4EKMhmBnLhJ_w5_o?z?(f9BDuP*6oy6 z2~0?ufBb~_y&Y+rHRoo=EesGa=+7~cT6q6`xBM3m*~g#NR + - - - - - - + content="https://firebasestorage.googleapis.com/v0/b/pinball-dev.appspot.com/o/images%2Fpinball_share_image.png?alt=media"> + + - - From 49b306ff0fd17df3b53b54b4ff57dfb455e99aed Mon Sep 17 00:00:00 2001 From: Jochum van der Ploeg Date: Mon, 2 May 2022 17:31:33 +0200 Subject: [PATCH 3/4] fix: plunger on mobile (#259) * fix: plunger on mobile * fix: plunger on mobile * fix: plunger on mobile --- lib/game/pinball_game.dart | 13 +---- .../lib/src/components/plunger.dart | 23 +++++++++ .../test/src/components/plunger_test.dart | 27 ++++++++++ test/game/pinball_game_test.dart | 49 +------------------ 4 files changed, 54 insertions(+), 58 deletions(-) diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 41005886..f9018ee5 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -88,7 +88,7 @@ class PinballGame extends Forge2DGame // NOTE(wolfen): As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually. if (bounds.contains(info.eventPosition.game.toOffset())) { - descendants().whereType().single.pull(); + descendants().whereType().single.pullFor(2); } else { final leftSide = info.eventPosition.widget.x < canvasSize.x / 2; focusedBoardSide = leftSide ? BoardSide.left : BoardSide.right; @@ -104,21 +104,12 @@ class PinballGame extends Forge2DGame @override void onTapUp(TapUpInfo info) { - final rocket = descendants().whereType().first; - final bounds = rocket.topLeftPosition & rocket.size; - - if (bounds.contains(info.eventPosition.game.toOffset())) { - descendants().whereType().single.release(); - } else { - _moveFlippersDown(); - } + _moveFlippersDown(); super.onTapUp(info); } @override void onTapCancel() { - descendants().whereType().single.release(); - _moveFlippersDown(); super.onTapCancel(); } diff --git a/packages/pinball_components/lib/src/components/plunger.dart b/packages/pinball_components/lib/src/components/plunger.dart index fa81c783..79b370a0 100644 --- a/packages/pinball_components/lib/src/components/plunger.dart +++ b/packages/pinball_components/lib/src/components/plunger.dart @@ -68,6 +68,14 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex { return body; } + var _pullingDownTime = 0.0; + + /// Pulls the plunger down for the given amount of [seconds]. + // ignore: use_setters_to_change_properties + void pullFor(double seconds) { + _pullingDownTime = seconds; + } + /// Set a constant downward velocity on the [Plunger]. void pull() { body.linearVelocity = Vector2(0, 7); @@ -79,11 +87,26 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex { /// The velocity's magnitude depends on how far the [Plunger] has been pulled /// from its original [initialPosition]. void release() { + _pullingDownTime = 0; final velocity = (initialPosition.y - body.position.y) * 11; body.linearVelocity = Vector2(0, velocity); _spriteComponent.release(); } + @override + void update(double dt) { + // Ensure that we only pull or release when the time is greater than zero. + if (_pullingDownTime > 0) { + _pullingDownTime -= dt; + if (_pullingDownTime <= 0) { + release(); + } else { + pull(); + } + } + super.update(dt); + } + /// Anchors the [Plunger] to the [PrismaticJoint] that controls its vertical /// motion. Future _anchorToJoint() async { diff --git a/packages/pinball_components/test/src/components/plunger_test.dart b/packages/pinball_components/test/src/components/plunger_test.dart index eafc15d5..abb42d68 100644 --- a/packages/pinball_components/test/src/components/plunger_test.dart +++ b/packages/pinball_components/test/src/components/plunger_test.dart @@ -121,6 +121,33 @@ void main() { ); }); + group('pullFor', () { + late Plunger plunger; + + setUp(() { + plunger = Plunger( + compressionDistance: compressionDistance, + ); + }); + + flameTester.testGameWidget( + 'moves downwards for given period when pullFor is called', + setUp: (game, tester) async { + await game.ensureAdd(plunger); + }, + verify: (game, tester) async { + plunger.pullFor(2); + game.update(0); + + expect(plunger.body.linearVelocity.y, isPositive); + + await tester.pump(const Duration(seconds: 2)); + + expect(plunger.body.linearVelocity.y, isZero); + }, + ); + }); + group('pull', () { late Plunger plunger; diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 8d76e8f0..732c2d82 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -414,54 +414,9 @@ void main() { game.onTapDown(tapDownEvent); - expect(plunger.body.linearVelocity.y, equals(7)); - }); - - flameTester.test('tap up releases plunger', (game) async { - final eventPosition = MockEventPosition(); - when(() => eventPosition.game).thenReturn(Vector2(40, 60)); - - final raw = MockTapDownDetails(); - when(() => raw.kind).thenReturn(PointerDeviceKind.touch); - - final tapDownEvent = MockTapDownInfo(); - when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); - when(() => tapDownEvent.raw).thenReturn(raw); - - final plunger = game.descendants().whereType().first; - game.onTapDown(tapDownEvent); - - expect(plunger.body.linearVelocity.y, equals(7)); - - final tapUpEvent = MockTapUpInfo(); - when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); - - game.onTapUp(tapUpEvent); - - expect(plunger.body.linearVelocity.y, equals(0)); - }); - - flameTester.test('tap cancel releases plunger', (game) async { - await game.ready(); - - final eventPosition = MockEventPosition(); - when(() => eventPosition.game).thenReturn(Vector2(40, 60)); - - final raw = MockTapDownDetails(); - when(() => raw.kind).thenReturn(PointerDeviceKind.touch); - - final tapDownEvent = MockTapDownInfo(); - when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); - when(() => tapDownEvent.raw).thenReturn(raw); - - final plunger = game.descendants().whereType().first; - game.onTapDown(tapDownEvent); - - expect(plunger.body.linearVelocity.y, equals(7)); - - game.onTapCancel(); + game.update(1); - expect(plunger.body.linearVelocity.y, equals(0)); + expect(plunger.body.linearVelocity.y, isPositive); }); }); }); From 8d10cf6434e753d8ab6bd9e0ef0f12a05685b9c2 Mon Sep 17 00:00:00 2001 From: Jochum van der Ploeg Date: Mon, 2 May 2022 17:45:40 +0200 Subject: [PATCH 4/4] fix: how to play screen can't be dismissed (#293) Co-authored-by: Tom Arra --- .../widgets/how_to_play_dialog.dart | 1 - test/how_to_play/how_to_play_dialog_test.dart | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/how_to_play/widgets/how_to_play_dialog.dart b/lib/how_to_play/widgets/how_to_play_dialog.dart index 766944b9..3dc2c62b 100644 --- a/lib/how_to_play/widgets/how_to_play_dialog.dart +++ b/lib/how_to_play/widgets/how_to_play_dialog.dart @@ -52,7 +52,6 @@ extension on Control { Future showHowToPlayDialog(BuildContext context) { return showDialog( context: context, - barrierDismissible: false, builder: (_) => HowToPlayDialog(), ); } diff --git a/test/how_to_play/how_to_play_dialog_test.dart b/test/how_to_play/how_to_play_dialog_test.dart index 24c683a4..2e3d3fd4 100644 --- a/test/how_to_play/how_to_play_dialog_test.dart +++ b/test/how_to_play/how_to_play_dialog_test.dart @@ -73,5 +73,25 @@ void main() { await tester.pumpAndSettle(); expect(find.byType(HowToPlayDialog), findsNothing); }); + + testWidgets('can be dismissed', (tester) async { + await tester.pumpApp( + Builder( + builder: (context) { + return TextButton( + onPressed: () => showHowToPlayDialog(context), + child: const Text('test'), + ); + }, + ), + ); + expect(find.byType(HowToPlayDialog), findsNothing); + await tester.tap(find.text('test')); + await tester.pumpAndSettle(); + + await tester.tapAt(Offset.zero); + await tester.pumpAndSettle(); + expect(find.byType(HowToPlayDialog), findsNothing); + }); }); }