From 1063d39de805a83169fc9ba1f841c1239be45da8 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 9 Jan 2024 21:15:56 +0900 Subject: [PATCH] enhnace(frontend): tweak game --- locales/index.d.ts | 3 + locales/ja-JP.yml | 3 + .../assets/drop-and-fusion/gameover.mp3 | Bin 0 -> 31346 bytes packages/frontend/package.json | 1 + .../frontend/src/pages/drop-and-fusion.vue | 131 ++++++++++++-- .../src/scripts/drop-and-fusion-engine.ts | 163 +++++++++++++++--- pnpm-lock.yaml | 5 +- 7 files changed, 267 insertions(+), 39 deletions(-) create mode 100644 packages/frontend/assets/drop-and-fusion/gameover.mp3 diff --git a/locales/index.d.ts b/locales/index.d.ts index 96bc9099dd..df84412473 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1195,6 +1195,9 @@ export interface Locale { "bubbleGame": string; "sfx": string; "soundWillBePlayed": string; + "showReplay": string; + "replay": string; + "replaying": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c28fde56cb..997ddf9c6e 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1192,6 +1192,9 @@ enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する" bubbleGame: "バブルゲーム" sfx: "効果音" soundWillBePlayed: "サウンドが再生されます" +showReplay: "リプレイを見る" +replay: "リプレイ" +replaying: "リプレイ中" _announcement: forExistingUsers: "既存ユーザーのみ" diff --git a/packages/frontend/assets/drop-and-fusion/gameover.mp3 b/packages/frontend/assets/drop-and-fusion/gameover.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..23b41c56995a8adc0033d46b946f967876073d1b GIT binary patch literal 31346 zcmezWdqWNb0pOXJme0Vzz|X+IV93BwRm8x`%EiUcFCro(B`>d{qN%B`Z(?F;X=mr` z?CI&}7aSZB5f_)7oRN{6TU1n0QCr*8)YRVI+dFB}^yzcvEL^yJ`I(?JXeEs_C*Z=<+If}%6vNF73=F~L z9RkYEyyr{>w)dY>pV5-USSIcgbkx%&LAv8nL>p(LftvLari7Y#{|vp>8yG}h-}}C_ zTTCZnW71(p9dBd%xV=?hU-R+s@QVfKZA?0qoqc_M#Kxqf-C~wEy@IAJ_to?YOgh>v zzCOPC-hcDG@2k(8d3$|*+}{5)X9Z1JYL}MweeX4$0)aAh_JjZbzh-|h!%qGngM)xY zfZ;&~4F(1V3lEKVR+|Mo&*n%N9=LKUC0SqSYz&7{M!;sR3p(mX4igSEIb;~P2|GVJ zD<$0CS;VyPDNoii#g9`NOkxiA>VIOLcC~2bq*<-ZGZRjDwmUL<2`t^?*ZZXH=v3__ zd$q|8DpHa`9e->-@7uV0(h5~|HpYd?mM#*Kccv}Tx~bcnCA~!?Qj)>b;EO<`f7(aJ zWQQ1u!cI$F%LzNSW{39&d~7#oIh-DNG{B8RbMuv0pMTr|Vgdg)PZtaIFP6Gu`RZzaic31OQDs}D2f9#f7ot<%$VH0~= zeA*k42d>G%ccE&!I88xFN*`SU`LS0$HT?TTtfc;-jrj~z$xDDm6x}G36$av zFfgbl&i3AU7L;*JKq)R`W@EEQg27~FY4#?D0*MNRls!vYh=<_8G|5;@5V+h1JroZy%dyw%HrM?!$%fd(VTfrAPGS}Y6+4;a~4 zk|fUXD6xv<|NsC0n1k6z$6kY^S1tc{kJ6vfxu-WFkPoFcFoa5QaoDJ;L*kU-@bb8VRn8jj)`P*hJ zIM~c!>M}v?R-+h;I-9!Daz@vwa|AMdr!rPgN=do6c%Q@$mQ^*!WOXYnKig;t7%XD< zZ@nfNvh;$dvWd+TLxZM!tpR#k>b8av4V;WkOkR?#2bGx)hie-&%w4%_+PagE%k-dlyCdQ(3$4zP5f<^XB136@4Ru=*8QA2 z&YH4Hi83*)Rr}~LInV55W!hP3jq|-VHCJDm;+4AHc*%v5nsff>tX&JSV+OBpANHIx~YF@My{Sz^Lrmt8qT(asvZ{Ndp4|i%!Bs#|g*WWRzyyNn#L`(6JF^ zoR%vPa!5LE!}pWd&lI)Eo}9CPt!D3*<6pgR>-_RvoP2AaknECObAz}3mYJ||`^s1K z(Z-9G2mC9F+jVQAbKu-`iHU*bA^fj@+p5i*H*@Z+S;zPDx;>nwxl*USc*2<^`AGqP zzN8tqTzgSzY{|20tJT)%R*_G#Vv!h5DUPlN8QzOgl2YWk}`-%Y<6?e6=q zzc1q3I@8K?|Nm>f`n}uA+}(kZp`n0_A&t2~Az>A}nslR2X3)E|dWI*7$9U9u8Y&WI zh(6O{FiYMNr)HtZl+eb>CYAgp^`71`o&~$OSPfRN&tPQO*?hpG$>G6)0w!jLKL;hW zeVkhE9Qn^*Go|y7gjllc1Leb<>{?9SEF8}6POOfO%1j5QF74Q%qmtaUY*9#d@3UuK zVyuQmmkrsB8&f@wDt11-@>73m-#ueDd*$hedAm#wPrLO;e?x)Bi&>0;J9xOXxR#$@ zw||@vK5TsvKl6Q<JDjYO?pr8=h!N|te#vtYaN^SAFkqw93T#gl7T$U?PT>R`(UXCK0kXM?@G7%pckNtO4PfOMX%f9ms ze`uOHLwUI|Z-9BpQLm|t&QDSUADx!4n3yK086A>oAeH=*SMO@knW+*BSomgt2;tfz z!?iJT=Ne_f)gr&wN?rL{nUi#G$z0_H%QG(bZ(@@P`u(!t#D=#sxDr$454^kV6*Dnv z>nxLrB1~`0`?m|_ob=u?{Zrl4%Db~xbsS>tGCD08emp^_g<*jOlSjji0|yu#9NUsv zoR+PKpO(4IOpWD%OM(L1GM)!{Nn#9XcFl{jN}P|0Hrz;Ll$;&oUj0MfcUIF7Kfe{9k2<$(^X~iG`%Jb~ z{@YmR9oy%c5WVN76gJiyJ*a4A8nfklE#vDu07WkZD`=b}2U ziCdfudsr4W33#?>SWd}Wd2as!{stovPJq;haK z9w;_7Fc30iOf^cJWVk@qK!?+?f<=SrqDP0M=oFidgflFszr5OH;1D5^sb8&A08Vm! zKaPdDbN{{h1)Aj4T?-}jzvl`-liVFo&tC0WQ5#QumJ;lo*u%{?OUQGZiG;__T#24t zlPo>%UbmXP$!)cM)|;1`%%!q6>j~fay!pyeAHf8ZNPl5Y9g|lKc$hWo z>ivJQyW1ZYhisSPS@%p>qamQDw|C{Py4f`m9iD0oYdlz*8TYURGary)X5xtrX5x)m ztZ_t7nP~=RpnxbFw*hmTfv8vt!iw+!0 zv^d7(aJ-@CP(a1uK#PM462~5O982!G^ReWi`ktFfPZT5#8q7JwTpNrT7>>?JN?yR& zEGr?T&L_pnvZ2IFfQ3^?V&%_e^Jbh#+-xS9ExF%8ddiF0o-^cUuG$c)a!caO1)a^F z{nmV&)V95MKG||Jw2g62o$PhpjT(%pf`Ke4j*lF;pB!?M^6HYXaBOI1Vc6yUbFS3| z=9O-@tokmm>s+U^Ejn!S)2Q<@E8hErJuYg9Q$M!#SNikv1(B(zs?AG3&C=R6Z@<>R zr{|x)|9bZB^Vgs6SMUGx?%&q`|Ns9!^{jX8>4_yRS~~I~cDEL{UJqtjBiX>f@FPLB zfg?eQK}dkDuyF%JWowz5s zOpXl?I2a%2r|&4AAlKueXWY?qVaZ`fb)N&WMnMMWIFc6@@n~v{4OSJAO8xhmPS}*uj+j$i>{8|?nnbusH*7NPd z!flG@pC0)TndE$C!{?arTi1{Ln)vS3-{xNA}It(2=>V_d_3_o_AG1z3l z6F4EZhh0I&N88~wkK%;J9^nnVJxmLD4lIzC5L@8W*idaC!N6+3aUjRQyCKft+o42* z`AmWaaSdm=*d;0tF70P^;j!pBak{5whgQfJ35f|7r3q6m9CG9f+Hv;Lxt(V&$@ZSg zIWgy~K@AfJgSqh|(^IeYmoqNexU$A##@SP=X4z!3IWR0sv(oBV$mn&0fzx0?xW%re zH~qr>P0gmb#jf(y<+1v8DtXH7<~7crx^gm$w_BHewQl#^RdMaH$#%{E`-9cBHJM!W zAAZP=U-Ef+X+!SWbg#VJ1ryGGp6z|!PWAiW(^;|Hz3cBsK0fm8B{GQzBL*e|oru(~Yj z;x4SBAuh~Vz?01A!IsQ!V&TkdAePLS!C)Z3l*UlNc#M%jqQybIn}IKZkD(>Ov++ox zMY~GEC&m{EDh)GI51i_(Vq)&mS#avObL;7@j-w0aw<{PN03|v5zMrg_M<)L^`2WWR=d8B+Mk>CzkVmTBxzs+6=DJHj`7EdR}i! zU38O&L6ph$bawVLsf3R^)dYNBteKQ0ZkQx9{j$q4z=8YcRcs2r{~>m!Io;@ZrX2s5@COS z{mkc&r}yrBe(hbp-}$0NpH8h|zZmg-m!U=icY{iTSOZ^zJ;RTL-o|A~{DR9$nFOQ~ z=dflZtZ#gk5YMnD(VV9xseXz=Qakg5geZn4QI-=2qQ%b~DrLS>!0?zsg&~H9C#=)NsX~q z>@YhYqj6bta;|09LzCk9`z#}Z(`J|}EoS6&Rb6mouFG?;9o@%yBKw|warh`+n5uVS zgN6mGcX!y;i@H;Elo!r3%S;VW*_YIoUz)bCyX3Ojv&A8&S6BKu`dz&(ymjf-HO7*u zy0$NpmKC4BGu4V!ie*`fM1OS1a&PxbtVb>V1Y0(3EIs~iYLn#Gn~wxDb!Y#bcmJt) z#L>E6xBgF#;nSPqcQ3h9uw?HnR|ZLEhBpic14Min71Vo}F8H0{e5lIvGbE>H`GG4u zA{Xqv7WhjDCZ03{aAD=KKC2*j4!Nr9;ppC%-gbi2o};td!HQwBA|9Qhon<=Cx@%Mc*c;N7ZF- zFx{+}_O>aFrTpb{%@;r0bNr3q$BYe}pBa`gE@q73Jj}x5?9BedVl%f5-(rR% zECC$cX&eU})%X*nk1-u!X>CxfSCeF@N$60Uw2ZO2jgRx$YR`0*18JI*(iEDW2{kWx zFxOUgxylaJuO4M9W_Xn>G0xQUo|2#tl=xv@l7dMR$GU_YVu=y~2?suji}YmjxHUdG zviOP9Yz!Ww_1IIOlQu!->d(sB%fRKs^;p^ zgYIjzAA48MRJ$9M$Zm7=bWhioU9%>B-co+ZdGAa9y|s^7TYGM$%vkz-I(L_wuKukF zCA&&iu`=|DFmxPHW^_5g#WdqUI_s4~Nel^Bx5?@pv|)d6z?tdF0b@p&1AOcPhn2Zm z4&@2{IkcCh=RhPw#No!zj7Rz&6d6ZsIjYdo%hkxErDN!^)VrN|nGfGgBev?Un}6b$ z&6Jw)#X~};eR1LQot26)r!TFS4Uc{NR%W8lls!6ptTG3joB3G}ENWJ3?^$DJ@W4_- zx+)?~cJc*h!_6n|WI0PKbeahr*us5aN~AO|>%@oBVp=-W^v-qaoI1uUGS$~*lDM4q zrdOF=U6W(J1;0twTwCg97%DRHl9S(=c2JVb?f+g{c9i)~Z~-*Q$*e3C;{OOwaw=C+ z4Cimxy!`L(FZ*Y<>;A9b|7W8Bi?ah8qXQ=gL&E`X#+Cz*1TrqIHokF8jrqoba7LZI zYD^hy$2b#Y-9$O8mq~PlxUncOuhg($32ZK73A}Z*dtuVy7vC6!8rlv#Y_QN%=9LIp zmC!GFKZ!Q7D=iK%3V#;n)t~7~+ z9-B6)Hh$mXikMgT?e|+vVccuZmWWx>#32uhS){NpAi#ry!t`wb7 z&fM7~z%paaqHNQfheRGIefm^uJLZY2Q-diOndp;?uyz3rY zQS|-0M4nWE!vaGAjS__rb_pFh4sLFaIiXGQ7w6we*T3swdQ#iu2hXg&wXN(OR`EE*$2}E|HYiXC zOeoyIpmGS5749`gNzYQ#T+Yk-9N#=+{f>WnFVimnp1x;Iq}C)Um0Yv+ z-;zUvMY9+E@3uSzkI7Y30*wt!Pu8?D@Eq`A>S1MAaxg}uC#BEh7#!*oyoYs6=uHG(ab%fs~vvjJ7=KALH ztFu%VzX`Nc$t{hK(b&xX<>sGf*B9!258GDOI$_acix(YlPMV&Vwp(J_w7V*;?8$wt zb_0`V*Q#y`T(AUP7tpY6U4MvV=(m>3)OFx+g+W1QK%Kq9eq z%a4tVGWdVC`EfEfJmJ0B&L$JtnI*Ndy@JQGaSm(b=^C~0TFF0Ri4Q6kWUSIPjOeJC zlDsH!>Wuau0y0yU@Ni1_aImpEcid#uHE6K@bS~+fca|dSjF7mb3k3yR&N?@n37r)R zF?jGrVNODV){dTC+=7gp6J8wXpV9wHJ>>YVt-n?(eC1qt?O2IpFPCc6l&q&y4L*d) zc^`eSU&;H&Hlbr_$8-(hPuSgtYjI3HsW_~uJD@3c@* zlDp3EZDEM}?psM8prxFu?NcuQkFO6v+j3pzdJ?zGw(R=VZ{nHSJTq<4#wOcE?(N>! z1ByNC7A9ZYkd!nf^zgEU&+Hy-I8@0QIOpB9NgwoOqYnHwSd{ui`qIt5Z#jq8E&H-3 zL;ByABU(pDOLB~S&MX2_1};p-93RpQe(^n?_5Y*UKJm};0U-vLgf8ne{xRIZ_SuWM z$Hmu7*n(w^22VHZ86h!)0~*_(C@2f{@#b0@ckVgFa40e9&?2AGvd30SbJk5eljoPf z;Psu4V}?vi(nU?dHHQ?+C6w8mrSx>C9Oh{_7p&OMa3HBw>}i1Pw{ZK_t2tlvtjwKt zwQHfX%bQzjidmwqFTw>{gqqWrdkVZum2$q(werZj&fK7@*LCkKR-9J5`}L;Ir@Z>n zCZ+dh+wO^TY2|)@Ja+ZWoY%WI*X_H#xBLA&|B^4KA1*&Xr(>E)N6oI-gsdm0O&Sh2>lVB;s$i8T(BprN5K(w8-+9AAV=UXI{UL`ww+} zU1$3HhxVECZ(iM4V|~5q*p{ndw~bx)#Z_n2YMRC>omnK;)ir#jvVPRRASplG(+echwm zLVZV*m%ZTj>GS6Em;HK8?$(FbH|D(FbL>}h_B)wvQ&(NE)0{rH@#&K9sjYm`kNpkp zmVbKmb=~Lhz4_Kx)&KFW+!Gd={k`i~{l_OqOU)kC&&o2HTv=L^cYRrDxBIId4gv|B z4LJ#d4c`)!8IL4*H>V|h=5ojknXx6)lglAdrrC*u<;MX{&I^acnD-oJW#u>+#i(#V zn=$4<8q=JA?81!9JrjhS99~S35;EAJp~qv8;d09LvZk&7j2k8|N)-PkH9C9{<&hEl z#hl!sbN6QRQHEplb}(oO8BBl2W`;Mi&*p9TecpM^ zMZ0L5)_<*a=EHLD_(qi_fldnB$;&y43o9n=IR=VcUmxetxt(=0z z-)dfoT>Az}a<6)R$d$V%pY+W14eeI!UWY5y**hZO< zgAb$6?KQm<|1<64sz2MEuJZRkS?$yRIJ*9oWy$xl+w(px)A=e^uiSb)%D@y}S}lrVv_fcv<`b;WINO{Op2`C4DN{8}W+OD^F+WX2geC!@>` z#k|xHFWXuVIdz$y_bJaY%dV&De)Y^aAI9JWK(cXILDheHbTjvoW+A2p*I(xWcx_!gdyq z;oXBT4SX7Q7>F|HFfy?v`EltbCb07)i8eB@F=`yXaOMEhjJt+c4Dz|zQy3T(6cP`t z5n<<1WaW`!(URDpAYr7@u|_%N#9@xc$9!xP6`napnVlSLL-7Oq#A*Vi_H# zxKNOfV~wSo);7Oo9;cR66t0jz;9Z_dDO_Pjy z--;SnpY~LEB!BT^e!q}e(d#`e^J;6|&LwzDzO8=aQ2uhor#C4q>Fs?Zm z&VJ+guS|t&e>C14+`<}gke?~%KsMurL+?a6j_@;VIoiw_agd*B!2x~7H)pCnU*NBr5nmrTDDn0(y4px}^ziJI#~*@(X{Vt2%KZeP;wb{v%C{`dVnR^{0Jw;EQl zsyII6;762l*ZWU5pT5tfXI-^^-J~q%JA!^K(!`;k~!CK5FVWZ4a!ns&5 zKu&r09G*F$=D98B`;?R|Cp0K49A;$fIUvi&$;-+kr)gj`;ZaNS%k@1Q1dSPMjyyP^ zz}l=gZ6brN+P>!Qn?GkxIUTI9vyGEwNdN;|YO$qI$5}^bvza}sY*_i`7#DJ{T;v!a zAi&Ngqv`$h>z11`M|#%l@$S&_5)hr@`+Ct`*0QtPm9I|7W#eI8(sKOKTaS807D=ZL z*_N9Xk5#z06#kGuuGz9KRe$MMhrbKGR?pn&*>R=rn|mIw-h3$o!5s01L%+CrrDrB; z-Fjf%c2}}ac0D`Kv`NM?R?+v^yE7vfEHkYrZ{7Lmb>WKN+wF?mRmzrgZGWh;;+WET z?Ynl3%l*yyGVi$`kzA{K&tc6?Cr%y)`vqrQjYa138mzRE4LI^iE8*!3y_REXoFWHJ znGYP;#QNjNNgj?v&U^_6g&1^Btw>mD^N-uuT>NPF+685;gm5#l^1B0_^pH16%ek3#U_?Sq$vF^Ec zXW3pRllFzCVUxEiUWr|MyX({%?;9h5xl*5XA}OH6&{1p0^~>{|{(Opc zlwR4A_-4Jety<@K_ZN!=e})+RJbNkdbZ-2D>0Y0@ZuFndP}lNGHaQy}JpJKaw-4u^ z7Dj)2_`2e|{?5FoUDtoQuD`c$ZT~fiBP*6~Jr(nL`R4FXQ~e&tHSc_WuDrbH|Nhgv zuGycid|Fh`mC*KzK_mV|!X}56PmXjabKg0`-nxpX{$Spjx(=R=Za!%*d-Fp0e>D4C z)lxGUNk1ij#f*6wolbov4B!>kVOi^>@ z-g13(c^mHE zKg<`oKl$BzyT5z?`n**rNKk4BOYml>O2}y7Nx02$AnEPAH^p^JqmrLBizV_gi6p#h zzLS*B^e3^VBPP+8;Y~`!vLr#Xg2=w)9a0GmG6HN8dVDi9cqJ@Yn41b7ZaKP9;;{oq z1gBAjnSqCtfr+pN)3qkX$%@GW4O|mX^j^GD(YT=F?_|kTU`#MYF_}Aa!)ya##T6Op8ui=+GABKe95N&72acAXk#VH znS8Tv&8j1YFD4sXY}T74q;?U`HN2Pm!y-?sJm;vC7@d%wE9`Eztn`o#Dk+4ww@w^Ot8`{h{mZET%B ziI<1gRsLXL}v)hve~Mi@gV0w)8nKY{l_Y7zC5`4BA8K-rLBXNi$Nvta88HA z0oGMVr29PSjKU9&9v!kMfWa=x2%Za z$l9}wd(xYEDzijS841i*WGuQfp{0H6QUT8|2NUzOJOgX&pSx)!Dc#v^tmSJ{eA!>^ z(vy9A+a4_F72@>zy0ubYbZW@Tq`ZkIr*UgH&${y3H`jgfuZ@u_uV3MQw*IP-r=3c- z@$SsE>pZr!+!qt^S^4m7(bJB}x7KWk)?ah6Iro!myGicdlhb(xOn&c4&ThGD-ftIw z{@?TJm%_)dAB*`e_g%5Ug3++Si{Zx!RyM{LY-*eiGHwpi>(~MegxUlJ(BF!p3L1PV9^1>AY z-^wH}o)u_wQR!%FIegS4L2I7Wq@Ax-_D@^jKF#37y+96z0||>h&d>m@(7e<4^Kg}8 z_n#`*Ac@-5=N$eY-@*n-m=X&Ddp+6o`De~(;o_em!nS%wO6IL&Oe@}SB zaj)m=lg#XGpB+(I%v5-5k&f^;rMpt{ORA?W*s#BF^W>bc=Nn|!hIQO5PMRM0;P~Es zDMvT_sto<)u>I;xgQr|++huNvvrZ%icCwNrNfhV zKji1tVJQujJx#na2M;*2_4c?ZF)-d|&p7N9EWNESYJ%Nmi=$1KMMV$y zyeMDsB}z3$}Sy-cu{qUjw25{n!@#y z7#volviUG@G4r0usWq<1t&d!^@AMPz7Qx~G>z2Y+!+N91tXGqSW?Bo(HCLUTxO^cu zu>lb1`bS?CmX$I+?q>)}<38+^n}zZ?!cQMzgVl$%FR3-W{?_S>6P%a`%HYSWsC zMK2D`ZIZdCQu=L0?%u~!pLNO?%$~XQTE@Bm+w#KHcIa^2SY94;Y>UH2dVhkm61{{nE3%GagjZtm-+_J%1b#iP9D?0~s zfHdRbcRH(9@3|24cY}B4QzPw9r@XH-Dm(?x>K|g9CZ?fN=@rSrRAhSC>x()a#vShRZKW*=9LM{B*NRvk6SPnS^MDp-&( z&s!nl$|BjNnLM4h%92bM7EOJywkWMwb4f+g;Q*IThKYeJ?EEvC7eyX?uwW<0oVv5k z3@i^q0^Hj#?%v{eVs((<*-GL03bzH`pM3vavnHb|4 z$=3wiR1{7WFg;9IEa+=&<(#U+!{xhWs?+nI^}CLV7^LyKGX$9wB{(@Y2DP8P@|N`# zm-;V`jk%>(JVY_wx$;?+*yfj&w|vv>?Go?ZeEs(2xBpd}e_yVh+wJr6_PPxU5}a-3 zYHT_w>#`0$a%cZplyODrn0n%|Bnt($k|PTe;-b?`7^)JQc$X<8JW4tslP2u&OopM6 zflZWaMu654Cbl%W2${w!Sw=h8Ok8HhD$M4i#!@0)9aDUIr+mPbwyd1Mh#9vg*WOI* z?`)Ijl3bT5Sh?mtg=1I;rt@uzB7AIw6DLg>)jIWywIX~S9Pv5F;4T*$&x7g zIA^VFh~Zi(LBHJOC*fM51uxcSCzT>ahGi}P3RUGJ5>KIO>7)n{_HJXbBe z5q!M9d(mRKCl|Ra{n=mV1tt7bx_9k(d~x*kh1+BgX8tzR^-W$}`ZBdO6qH1a^$ZOR z@Jw8WGBEh5|72y*HsEqFFko*GHQ;YNVi+{xo3Xfyfh=Rr0XL?GgKnHN4sKv!_?p=+ zs??W|XmCK~Kmt31o~WN=gU*eBu&nJ7Q??v(EJ|RqUC3&6VCz(VA?95d{;X415Bv0Y z`cYkGhGohuGft>+JxcPtnq?em6n8qY%F1)K(!_$7aUVZ#-)C(kGtXnO&yub479D)F z-hZ(|5>M@h9r{Q8mS6YDa$~#EH7)5kXPAHg^Yg2!OJ3fadu*ys*Y*#wr)6ht>sMjj zCRS#ivt{b6_nONiw^Y{zmww!|-|YGKTaIl-yJR- zE-&%<$Q^UO=f?&>m_o7#liDPV*?k1g94*N0N*jj2NuUntg_wAIYc@}xJpc3N)CSAr*Nf3annC%bt?l<4Ay z%T_a)7aZSc(Xq}bqcJ*4Gdfu&+fZCIZL`xVo(Tb!+g-hH}nXB4K+zFwzhfAz^eO}(OTb(4h zL874PK`;+{50|pe&PI1OPbJBzPm0P`6nb1>PG;!vkz&o%iceCwe|pXmk@6%V&g3aa zCSOTmNDegYIK(C=J!6VS(!;{`CKtzPa*rF2G=G@lR9iG_o|Hk6wOrqmBmMnt8XgWj zf%1ObtNYle9^>5|2X>yAZ?$U1ua6tA_={Vcy;yNI=cCt-slH2h9rrpTvYYQ(>WuO~ zZ?65c*mnBok6FEUX5O}RTOS))zUk~emxiJ(jtXKdOa~NXo0v2XE@?lTzw69L!L>bw zOf`pi1e}|0915Ap% zcFoTH!7@b`OjrbXY>Z~UFi>b~SUoj=y4_Yo)2>>HadWBYukB@q)K}qgQ&yOn=PTYTzU?aiGTOae} z=e~uFk+`^AZthz0C@UwoS~=!i&depB4!3?^RXO!kyT*pkneTp0xDn@itbUL6(W{$# zBd`D1YqRTvg8%Ah)8);(4zFMKy(&Lq>&o?SyVK?E>&>sv`@9OC3&*3VypuUf5~u<@?*%77y;edf8Xy6E*R z)T_wD^GelHyA6*j9pycis~P_dcyO#m?P}MN&sUFVEzDj0n#&?+Ma&i6-t#Lq?Wz`y zIdz(6!4-~;*{ln1SNnaN>u0(5SJJVxb+Kla>80m1=e~|^7Y_Z=_`Uz@jvYte#($li zx<2B6#LxTpk1cwX9iz6~*0RBb;l~TkHij8oiy0gw*c0Z?P;pL>n$#63X_@d-LbAbi z25W+e)RGi3?^n9T51yA(7;; zf@6w_20NdFQ?vLo-`CAn{vP+9H?#6K9@;A9>25gfq-@3=-}N@DSAJ>ia69$c&7*u-!&?-C}z*E9i>}-nOuE-v-X+Ll^g&4 z9({}NkFMG=>9~2Dbl}=A6M{~Pe3QL)Ix_m(;VE0UE!4zP%JH!_YzPo)V3?H^q3pe$f(DA>x>CacaQQLlaQ;|kG|E2mhuix*55Xy8p(I$3bU zWtmlqfKSVlrroVKU0G~7r?#gjznpN=(~vXMLV(e5@`))6CM;TgdUdzr!N6!smmE3g z>DFB5p9yEVt5&T464f(pu5{`AywhL*?oB?Pw6*Ny%}(2$U6;-nZxokv3KYryc4gV# z4NF#k{AyixWBb?j*>|=|Me6RZeU?2vYv1EE^U|A**A(A2UG&x7vwQlr-Luj$PK4P%+bB_kPU9+@qw#A>YH$!@+6v>dE`k$+MRAtV@y*m)OFx zf12#H-BCTO+sgXaT>U%0>)>myrkD44*%Uuqh&kBmplEX9urWsyv&NI$tnf{X-3w+3 z9<=53wGrE*@wD&DzBPsuvx8TOa!sE4Y0vIO%5q%cPk)vi@Q+B&oTOYlVT#d$nZ9Y0 zN_lORjXmzoYg*~&(01x!8M~FqnmC@`srd=n2_5T$z&+M)JwKEx4{_g4E`V07>YPtm z{6D^h^;kVVtY-7lyS;fCtJX~WZ8pX==CP6OxtqSN_;J5`*>bT=(b|2HarSQsWHJY*?IRBBd8jA^h+V4m3PzwB?y>=m;T z8x;~HU3mm-97`7|vrAM|q%0M@%c;i7Ka(Xa`N50{3(w`>Pk*~qzy0F*%b&x{4sxG} zpCHf{l@!?7?AE5FIOBqojbPXcGv=7GpIVAHo}4mKKi_}8jg!6c#-vM<)ra|G6g%rG zJhfyN?U1&zQ(2<6M6;8F{ZqOGU&pril9!*2K9yc;UaI-)bNTH=)2~VGoH@GIAqg!T zli!Ko?3$@ByJ;zF{h_sGmrQ)uhgQS{a=kk4)7TU7Z|b_|=O2Hbe(dsI)~DQ2#Z^Co z_x^0wtP@Wvygp~%-2dA{yN>t%@t3;Ae?fxTL4O8Ig0ch$gXau^2gf8L4!BD_cbqpP z%wg^f{)7k#wgcWX!WA?mSQUdMlpYvK#8_qQJUN{`O@)7<#|D-I3q+;E3>6e&Hl=@N z+##|ojz=O#%7owlz=L3$9N`Jfb$>05B$rLtCuq2#)xnL6TOvjyX<=tm!-52zBwhL9 zC0A#3|E!6MIXZJMhk?M%0FS5%(-tQ>SBh*AOl-63G|ZS2EVJUuCF}IgX|n=#CtY^a zwES?fpzMKuV7GwGEd{;#t0JazwVzm;!d{@fVg1_WR`RaN|9@}F`Ns2OZ-rwwqGDBP ze2Bb|WVOB3o2GU(%)A`jy~o5wf$0Vd>kZBxPc=z( zi8~?;DVcIz$0q%m%hn{ZJ0)!XE0aeXJ30KW^6NhN@nz$*guN5)>M#HKj`#5-6ZY_M z33fKc*_|m>Q{|>j^i1L_Q1|YV*&+Pt-m%qQyHf)ey@^fGl-Jz4U9@)oS{+{|<+tk6 zGZq{@x>j7jKV97-_>!%nQd;toGr69xY_~V+M4wiXx%uSd_qBUVBhHGo%bT}*2OG*S zT{-*eEIDP%n=}3HW?V>)&)Vg-H6>?tZ?fyww_9Da9!6%07e03~_7Z3;Y;X{mA=6~o z8jvt^!t<4%8fHp#H$^r!uu9w#`q}&8&x{U+jt$l`wAo)aCGct~91s#=P$=MLbztpu zZevt0Dj3VQT%&MSqy5nplW z+_4K+gEN&@C)&+Ro*>4*f8x_a`R^jPZVR>!4y))lRwXDf z<|HIEFG_L~;LEe)nUW;W(vgtTcrSv*;m|vtH3xY(SPnWe)*RqxICEeVYtDfK%oi>* zUeP$vbSCHH`~?OPYpyQ!uQqx4l=r}o1Fb$00mmDKll1NdYG_APKFQt`_0aS34H1__ z#%BU{DbL)5f>_iHA1>x$D2rj+>i2tf$XjlFlE{A$U&bKFm;vAHehb}LScOq}UBwJss?kLv3~A*-LIuIbb6 zoRMsJPOoj*_fZdb=6<~r06Pt*=LD|vYW1N`NW#%(9(5H-TU&7gem;hd-`U| zlEZv24(y!FDskgfzWTu$@g?)MFYs8)JTZHJ_(SLI5;L`>;TbWn%V&tJySUJk%W;>Z z#hXbg%}hem1AMb;NzAJj2_CJt$q;WqwX-NhtWa(4C@9aAISsVXG8>& zdAL8MI~>pwW|)(p-l&n#(xj4*#&9R=$s&iBixQX~9cdE~U}G>)C{|E1)hte$W@6oOPxj2WG>rRwwUhDlYqhkH<<&<_-L>Njb46}mz2a7JvW)%F(u8Q4HNU+# z8o%(kzjtS;S!J%qrz*u|%`e&_cJq}Vbl!A(_naf%uiuGIlV5ntu9HiHFQ+}~^|{|~ z*NHt~IFPbinPU|jOEOb}paCEtPwSTI?BX?Tuey89dnJ%+TidTDoMI3Dcf`+eo?EUxSY z0tXx(^D;0hZsudWvBa7;b|=@>)sh$9t`_QTZdBPdXV-#t7NY%=j_kSqptI$RX~|EH z^@6r*ij-emn!9rGGS(M4%WuZ(*&RRT|HNFc;^*6NXYN&eq535&?it-qGv#jC`d&9_ z=gXD-M&V1sZyj5k`)J3-$g8*B72Pfgw01W&x_b5J=g9`AUi?-}OWZ8K`p2iAtXkz8 zRZ?#qWR+N5&co^8EX3&G!Mq`Wqm5O9W0}x}|86T5-b>RtV4KG5!F7yb0@pJ(4&gSD z9nNX|1~zVj9`eVSRxq)ND$n%&RJiTogw|!!3=vrz+-ls`ssRZScF9f~mnyj3s+f6V zSJK0iO)iej>2++m%O05jJlb^s2j^7HBbQ%Yl|Hs%ze{84DuawQhaVWTvo;=DzILXm z`n0tsYyA?gmHFB7cougZFIl=lJpJ)uL7x+80!y{H%2ZDJwV1BxfAP-n)^#Jxi&L}Q z&u~8T(B<=4F7jD3!O~&XDiv{AQ)8Xjtq1>1I^FwD8I-wjW@wU=#{;oZBW<}7Es?zH9#tsA_P zkL0a!d--tX=NZAET#{_|jcaCRIoHh0d5wx4LD?Iam)tepp&GF@PJddj)5_THzC5edSE$nHLjgY`bx8S>11znY|zPto2rZ(UTr*XCu3hZ!bt7zndWW;Q4>$k8%hJ$;Qq z2LEF&0Vz3_$*d2j7)%ipmcFQB_PLpb#e{M5au+qVL}O=#6*nRi7BHB&9P~cdd*$Km z3lTyrHY$ID942a%UQb;+@n2nTa*Ly(^zjY{1|D{U4=EQzx2}8k^HXBO9&>r#XOk+Y zhS{Apww*LvsC5BHMJ`ox6er57JT&FKS?jwC+x@C@;SFQ zx$iUd2>8l*O6={+%H?aHUdi(2ivL`5Y0AB=y;VUsT34YbIVK)v2R}L*a%M^=T8&+2?P~CiNqpioTKJ69zBquGZJECCB$bH7m z%1|QUK!UooxMch>tI{RMFa12|GWBF-kNwlju5qhRx_B;>`EYu6>LwO}lz_7VOz<7?!qEc9r7Ol`3tkBa#mF zZ-4Pq;>PZ-jlEAk6!N9iSOk7F+NN9=vSC?&>lwCH>DIlv7c-up$#UDlzdhR2#9)6l z6Z5OP_L80Z{GQc(`+D_;wCv8UxzTfvgs;=N+x7a^x*UtbQdt%jtrMI~-lYayEY8dy zJ{t(t9R8QcX}E!FF-L&2frIR4E`fW7e}#=%1H`x)6WeA4bF*+7sP{AkHgIrBbVxBV zEu3W|5ztXqU^(qzngVApt3-5Tkh@lGkQ-Y?($1LjOEWfb)?Nz;S!d!F?x)=J&2fjv z;{4{l_DVT|C)O40F!JsZR-J6WsiG|GZkj}it&opKcT=R1m)x4TB#%!9(^&k@dab^+ zPKf)s^2Z%PUYVJtT*6bCXK-5Vl)E7J&?+vw+r4THD9L^A_!(VsfE%%pL@D|TDf!K$J4tm&RLu-y4a%O_uFc>J2pj?%(~}HEzVcZ&0S>uTmHei{dHcea4__r>nmsgxdN>=e99|&ZBh;WNA}~!w zCb5Cxz*LXKgtS(vlo)oA8%Yh#hu^HNJTURa)u>aBU#c^%e@SfCSGlGjXdt*)Rm`9< zSXz{oeS1df^Xj}*Z?muVUsa|){T4rii@EI1jICW!o+W<89nl;mQ>We0VqdzY$vQRR zjjaO9gN7+amYVA`D=l`|3Q00973b7c7P==G@+~EQa|oJ6T#Qs|kYZw7xS+=1Fc-7bi%EvLT#LCJ z`1n|pnHr87dhkEyElA_z5zRB;Wloe(Zf=l~VcvY_aUD}|r(OEK8`OtUc8A-K)N4wu0xywuO%f={z2f+Bq2i$MZFpNw(eNa%f96Ek>_b6$iuN3>M{mL_%~!8`w_(w< z{;Jo0o8rzmMawW%zFk~7Jud6!2jerQKI_bO8-ANN@7cBc*7h4;n{Ag{8o$|Q^4qO{ zwx0_tOUpSk?;?}1QtB@BB*)H_(8Jcl$dGej4kwQt!<`H7SMvN1esXAzCF4P*1P#VE zLB(w;3L?*f850sx*f~2A(zx7|9t163IVFH$=~A{v1_|cJMu!<5NSH`9Us!Bc_Bl}8 zP)I~PqsXk~zGJ<9P#e>O;QfBHvUglqmFcj;MxvouLY7CCk$u^jO?%v=yIgWEY<=E( zs>Md2d;L|GSw{pJHfx@Yal3nbo}ps7qU9d36u)D_-?aV0d)zBM4m{8NymQsp_&Liq z_FRgR<$hrp@#Ey;=1Z%-GS4cH2}n!XW~B6FWlq(r1@uu<8SJVDnH5GPe6_YSAcTV4ZH_75w)M?I_v5_+_Jxzad zWY<=uYf4pXI2<;xFsLy#2)vnL$TYJ@%SXQDY~vgTi5bR?oDCh^S0kf3nzf z{-n@3ecH>FYM))@nG`RWr*&lI=_`o~*u)HYc|9KlC^vKWyFdKaJTc_OqHvY@o!XwA zmsr*bJ4AjG42iPgXWe$7!$J4}drQTPp6wwoGFXB({N2&{n{$Si(DWr1<}EM27V{r9 zu-X~#@ay{ZZDB4OH-^duftJgC@B7J8d4TzL3VgzA;bR80BzDGU1~21fA%2b~hiX_)k9@<+smz%Ie^!`6jn~Ojtj}6+s+qj_xEa`H@Gxjeus>ksVW>-xYV=8{W!aQ0uD>hAhczL= zw1F={vEf+)AM=}p9EP@p$IV9)x>_R=b~V0Za_?g}lb2!a>TtH{5W``9oe3N)2?B`* zM_7d13m<5R>M=%(6vF+U=7^>vTnB{5-B?3eTy z8%`COk?yA?b9&dxtuO0O>!^t;EmYK-<$uO4K46ovy10b1)Uz1nzRO>?1}^XEJT~`W zpwYUk#*c*BMBI*=q^r3+nwf93b5-~?1-WNmSgY3Qga#Wu71+=fvTj+-oK!yNH8$$Y?vW9%|(DMjpd+5!Y%V0$t@tanRCj;CKMqj1Rat7pRp1Wxdn<9LA1i16Hfbm{vnm)l1Y z4PyRG&bg{x7`wY&sV30#9J&qwLJB5gjU6I?Pz|x-+kMqEY9{V2uxi+?`Nn`YgTXa7T2_d%tveI zOiMH>R9ftICRAdPgY&&Y-N*BSDxEV3puw3~Vk4mtbVxA;CI@HE_$u z1a{rm;fX2>(n4~w{M|&kay!)=f)qHIIsG@8ZPm^?vn!s>>NPj7{j_)=&G_RN-CGzK z0z{dFSPcYNb~Vf_xzl)I{*qSl(pw3DIt7c-cF4WF;qUe;TDIl zNu|o2>~H}Qxrr(6)9ka?DdaLKElD{pp<1`DiiPRf?PQVMx8e6lt?PA7pqG4#XJPv0jwOlBnS4+{t-O=9c)% zHj73khBJqbMEQsv=Qw<1LZ8nGnK$XHpHIpXjPTI;$Lh?Z!8n#6UU)*b;Rx#v7T zO{*M)Z~MU31goxn_D4S#zKiv#_X=-DQ=2P&?t026;^*mV-syaJt>fUA9rh90j5fQ2 z*Iw7l)7-xH!u5xi8;n<^g?q+aKQXEJ^cKy%Yg>1vn`Ll$^a!Y?p6(WUW*r^<;;iaL zy}PwLwjWNd4|{(4P2u&|!hCa2d}3Sy@%(*k`F2mZH zb06OfXXr3$nDdO0LB@(*jpLzC@`|S8`JTs8u1KVDJV;3p6HL=N_$i@F=9!3t*`jAm z3tlC3ak;TRV2Km|u+#gC0cUMbu<}HXTdih?ZzeIdw3|m=h!Oe`UO6Lw-}RL${M=Ew z0s{RSehxy3u8%gZ-X13rB05!N8UJS4-c3IWf3FsqZf#Q3@pV=INdpcuCEf`x1qDtw zm_#3JQ#s(nuFB}S!mG(imAN&hMd#r}<$i-bmpnxRbS~y9+Xk$<623poDKt#a%Vo(t zkHhKGiNcvy+JAPIT*xU6aLeHa){b)I}y|Y!MR!Z6;=(yqryON17#15T^SmU@+A$Iz+pzoV*I_;RU z!|&9dvSil%b+b>DaWpEP&PkH5lisbo;?DF}?=H`lo_bhlh2&CaolUnt{5>pF^IV!| zU$DRMsnqYYDpUPbe}aM%6(i5@w)U{d z#}ivNRvoypam88>)i-SmUQ9_d`ndL0#GGX^J*I_Eq0j`z(iW!T*JU|0Gj-dB_461-a#|Id_& zx%%&wT1uI`g}JfWrX#m)+_qL-+8J?{EBx)nX;n-8x^tsquNM7Wd3aZ@Rot2l&xEsM z)(Z*0Txm45aBj(?8jIc2-b)4+?ezKAWqWZY+l{S4!oIU_#KhaA_x&=Hl)Uk9?Sb~x z@QS~0V|HJ&=<&HwV#KiMKnEksVwQ%@25TLdcSfx;^OIBN+%VHXpIMo+u*k56_c4c} zqPbx(6NmA`&rh=%wIu?I3j!}_IG4HyGs+l!O3q!wv4^jisk@=^MDvsLt7feJ{_*F& zRqMY*Nw?oz#hD=KSk}_oz?j(N#=^39@p{Px*F3H|JvbH}l3tyWzOK{pTJB0w-zB{< zO^T@*O8kpkpZb@HJo4S}$4JPvYNytsQ>Mp^Pw-87=iBxRl;ocB{&-)tpZhj^=Yu-q zs~`M`F%mUvosuF;Z#LV|TR}Sx1{>S^Z<@)a=~tG#aH(6{#&e|`H1F?m_PyTw+RrcO zMC*#%GiUcXPgGg>d3Dj2C7}kY5(3}c)j!;r{9$I~)R}rK9BVq3I8WkP{%_t!o8%0q zx~tEn9&2RYSvbvkqRy_c)tCCNe7HT=CD$=>>CWHY)sMHfHg33S7;%SJY$yK-sSXZC z2L?q3F^L2{9yb;xrQ|zp>OpOLk}mMGaV_vmFk?CgOs5E@a5a7{b8^+o;U(zRzqiz26*5kWX+vS&f zZ3$UwA$ZX8^4s@q#%KRdIkjMS)x`b6f9F)hbNarx{AYfVhQy5-$6O^JddxVHa4_*z z%#{#3w~H2)hAg)Vw+b(u%3?Nem85cNOY*cQfw?Cx#ZA*qiqKMcvNUv^c4&)URTDq! zve+&9E{j%4_9$irev#&sRd-(Dud?uLo`IWK#HpgLmSEkgH(XyjyOm~jgbT`SG!Ar& z=vHc+p`0-F@R^&tb4)^CFewNah%*E;6|fqZFdb&TP;RKk{n*X&rNJDI&DQ4P4V3 zx}4gz?wKkB%Mt@=hKUoH3^W;p85N zCa*Hz1?998aqY#0Zb~NpDLYeoD;fkt@6Gf&v}9)kJ3}IqVuT?FljVuI)9zj=m%L`O z^3|1%Rm&41RV-efUcjyOOyjx)a}*=5V)l9-LkosA_AzB9UVE8NG9L0g*(J4vY4ww7 zkqW!FrBpfDEe&69p(b`*q-DBTz*N_1dC#=v3*Be(ulaTOXt%tH)`60piC4TAH6Dw# zecI|S;=Nq+^l4EYtN7wyJ3Q4ji>AAj?)fX(^38OqqO9_=_P#0cPCGw;v2`x%4z@e5 zXZ=FfJ~?F9^L-U9E1S*wl%k^+?pauqUa+*{o6*}ji=3uEPzYpb)NE*w<8a(_sFIhj zQ%K}^joQJ(UI}bmZ0rqHNhK<61`ov&f9V{PTyP;ljiKQJyMYrIvynTq7GullA2MlH zEHNUNIT#q4B`+n;P*NF!WFS9Q#YC3)D&dMVi zX~)#3uFjgkerTnqW24U++tuB#7+OLYqS-tWSQy=y6tog_gpWCTok)q3brVg{uwiOr zP|!=bC78ynD5sWkkXvw~$&#~NQ@%jB_Gp|li|c z_9vYR-1lwoi^ypHHH)MeK)X43(pVDiB$Uahai@4xg{@uI^H6GPS@ILb%X{3OUhZRJ zS}bfZ{cH@M!3>LvL!wMGIM~)O9GI{n;J~rN0cJfJ=S>#!oIg~|%67(pF^Tg;09(V> zB{`wdvo3#me{5>t-LMB+`!yLNgf15!U}t9i;V{X0^P>|Al8(n+qLOY}9OM<{kaBNZ zZsQiJ6=U$}d0sB#4&yDZ4qluyO?G}RpZ{}v7zhPn-;S4a+0v*sPB(Px1sMxyKyer&c*^Mx48(6Zz_ce=#d;R{x}Q z+x)G4=aLM5r_?j#W!es;&9y*G1Wha!BSQ&*)Ejlnssm9T%^|*Jy zja+*frsLg_x+|>=g!tK~os3@E#iucKYS+aLnVk=!1yp{XUK6D3dO=5h!-sisF_pbD zx1@R7tUA1yEnIq@*mbL(r<0F6F)}QQTd+WNhFB|O<4V1mzqSVU7nroOOlXnd(Qn;z zK!V%H%;Cs!5z}4#Yl7ocCMbn8w0g&<)rIO5e3FP=E|R%cZ`v!K>-X7M&SdKyi)Gr@ zTNZA1O?>g5i3*TW;pZoLN^`Jkfv3hZjMn zJ7n{HRhM+t9@#g`EBVLMf*j{HFXQ~e`9%w7Og^h_sOh$R=I!a}k?-a-{EQXWdOTr| zR&eWChBemwQ$9U?^6Y6$z!aDEnF~ICT3WKr?>4_tvcRWkf7#UA+LnHHm-a5w5}CA1 zEOV3nj;RU`3>oYU4J)}=8Z!z%6AuTs${U zG3ou@kf<>8RNZRhcUf8rI~*=%2yb(joE|at=mT}mlii2&qNhCGC7@%gcUpDHqBkFZ z1Qh<|Oupi`YWcZ^6B|Dl6)^0GFvxQc+R^50=&~hYVYTa$O`VayikB*H6nL{;MlPl^ z#UoGmGP6nUj^0F1E2iK}r<5<7o|aOWvGe$Ov59LlJNQFZt>(JmDROCQ_+tM^d6_WZ znBzQBnOa3tKKoYQjI_EgHg8+eWRL!f(rqu@lTP>PwH?1G=I$0_;Wsx+P^aGcis}aW zpb%v{YnfL?MWw1uzttblwEe1jbH)V;<}{`SN(>AM3|#zd8@n3+&S{Zt>|T(`40I z&m4v0lOui=#|niVDm&pL)Bp6O%ks#gfGvxCk7RPIpLwOC8uOuUcD2*gcWIBCC+gWN zujbw@nIo66R0Wje9`XKgs@gaCW)f_WL?idjC;i;#uu@K2ah2JZlh2OTi}rQDdT}v) z#RmW96`_+qPh?x=zT)!9Yo^?S3n%MJDlJIfF+KTD{oaLTw;ntQd#h8ec#2u$w_f_} z?xaqaSqA;8%2Ew46rR7dw29#n7E;(U$4g~@*xmTb9W zccu5Z^^?z;X{4jgaz%W5T z;`%K5n1Dyb(OyhAljwH6i$9=&|%f6<494u?!nuU6dgv-eHmu_b;x z6qm)gUANGi8ldqq?9;2Mo_9_i+LL$K&wRrxvjppkRedw3UN(wSnz>2GHR$56eB0cx z?UK0xv+ti-7$#h5{FSTebXG+9VbgPJDVuXX8)_P~x>{ZF={A%Q=Xupp<+S?+U!mmF z^t8RtPMebb z?s(RW?Q30)CcErqOz<_3k$D_GrPWAes#2O+Bj+(&u1X0uW1ak*1&g-bNRc@1+w0T1 zl;hBpg}!2ub;&{3o^9~f*{$kOtR*UB+_T94(gFW~*GG$jrz!RYPIlh8pvz5W)!~&f zj=qO0xsn3-mvokJN?M4tSQg9)=Ucp0Vvfj`;Ln;9KA9&AAMVwfA(M2c&~VlozocnT z;-_{tNqNhOKUHMx2$I)x@8mP;DY;Y=$rmnKEh7y|a*um|Y^$;p-dzbxa_X6{KJX)w zoOFj%uS1^Fo)#|m z;_%9l=w3*(*~IIG8X$O$zQ7UF)@_bXxDp9?`&uAJ6f9n9}~(+=n+t zZ%S@5i_7=N9&0LB+9|#?cK-EOk1t)NS@7-T6)df%gBOHUw^kb5wLB=t#{Eohu2B1A zD+P^?eH9gR*7@noR=jXK%^VW?y^ntdL!-z9rf(m5J%ElF*ro z3m@#{!^~RBq6K=pO>H7-DtsKDo#{KBw=K@uT3uIOY=+aV6h)l}o9c?4K6LEeEc*C} z*W*5yoh}6qJTlxfbd)8!*3Nr%YVMY@8DiRu4bJ>09`kWK&zms)<5`o$g9{?(Co%C% zIQnY3ai)~Zx>jXo^TdXRG`5Zxt4V1RYip8g{DQPjAm?+JGx`r^3X10P?CGZ z`$N5QAM>t6*tncV;;VOhFJC(_K<>8T^t0w%$sv5DSn^4n-|-ZQl9?)R@{DEHvptTy z^dN23?Hhi}n_d+4hzTV{$)wJ#shTO!6lENI@=-_q0#1o-T0Nzo9mTx8SM4}ycRuOa zy&p3sI4@N5>MSy`KYn6cx6qy^G3rwe)yaEjbS|{qA~UnVQ=nw!9s#EZZ>^%+9DfzK zy6cH4_$CPNv5xvQ$mIh7FnDQj-X|@Q9$&=bJ)q_Q;L2f=umvqmb zpL2iN)J~z|?Y$@3UnPHf{zb*9bgkB|Oed-5!8@C_W-_>6y)-e)Ai*)D=!;z1f<+Ci z*AiyTOS&MDsKKb?{D486joHDNnc-*Fztl5R4;nLEV33~Oblh)x=glOa+PGCGJxe)e zOnR8OlKr*mmD!V~Xen(B(qs-j$)tVp3j3=C1@n%6d7An@ z#9jHwFNG%&8X*zRQ#vO-d2`ATl;oZ^{HUz5?VeeA0J?TbJ@M6ta}VFcD%PVrJ*`6a z&st*W(HmfGttJbNHJI*Xsf70+tBEz|{)+l-5vxQSqb>=VGs&MDX zS6y|ltF~=5neCy~s%d>A)z}`u^53_rDoI;sXvzcb-RtX%K;G!a| zIIYc0{3DaPmzoCSoYa{wHZtm^HB6q%^ep;ROO$}F>P89aMLGiNPd4mcd{VG2p7BgQ z&$8gR4t$D#)YTIG)L2fuckBB3gX2_%ar?@U#cIk^J+7=tQS0Y+`nBA~!|0*QkDW?a zSvGEY=F#mXmSrZhqVP(yBejGt^CA}uE_J$;F{c8+T0nlydcQlQu2Jr$_-2U zl~N;vZya)(k+a(TzOeFAo#!?L^H26X+!^H~q|K}R;j#RSvvZB?RxN36mzq_~xF&6{PP<4*U5}2c z<;_(Z_A5h_7}zB@YKduF7E^d=a^~@zD3+FH>7_e+YWqI-tT680!I8vd$e_e5;5vg# z#UOpW1E^a*^R;q%^~v#YF`cfhwk zD3-qZZsEm? zl@;?mw6&a6jh4(QijdQZo2NcgTW^ZI-sD+2i(NX8dY^pL$-Gu<=A8?(cJg?y6j>3u z@#2MPRx2F2k8j@mk2TgcAaVQBKiZ z4yE0Z#T!IqDMIyCKMtZ-Ff>RP|lpDfWKY?F3O z<=n7cT%u!AQO4w{36uF$vr3(=w2M7@axZ1;Tm$|mafxHks^zhIzT#q!?zAd(Xl=}CS~lUi%IlS)7nfRk_jqK9U1t@%WvlYU z$^F%*rJXTH7RAgp$(`5CA?jv$>cKk8DHe$h$8?u>zmZ>bDLyx~ipAe(eXNxFxC1k;IsW>eK#me_P0x+v0MZt)}3Sy)8w zWM^P#KyCfMXFFFaE$Uins0ktsIyD-V1i3C$ii!$W`UH3^Df|CR|Ej&KOB5){Df53> zRk~(llD`tPVr5`pT*AP>pa5;l?bLEHlCy|)eV8q35oxl{!=pvvgN>O(pV-99t&@XZ z1|4;4>t2v2?zv>@q0-XQ@c%vQR{cNw|NrNtj+M#X*Z -
+
@@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only >
{{ comboPrev }} Chain!
-
+
-
+
SCORE:
MAX CHAIN:
-
- Restart - Share -
+
+
+
{{ i18n.ts.replaying }}
+
+
+
+
+ END REPLAY +
+
+
+
+
+
+ {{ i18n.ts.done }} + {{ i18n.ts.showReplay }} + {{ i18n.ts.share }} + Copy replay data
@@ -139,7 +153,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- Restart + Retry
@@ -168,6 +182,7 @@ import { DropAndFusionGame, Mono } from '@/scripts/drop-and-fusion-engine.js'; import * as sound from '@/scripts/sound.js'; import MkRange from '@/components/MkRange.vue'; import MkSwitch from '@/components/MkSwitch.vue'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; const NORMAL_BASE_SIZE = 30; const NORAML_MONOS: Mono[] = [{ @@ -401,6 +416,8 @@ const GAME_HEIGHT = 600; let viewScale = 1; let game: DropAndFusionGame; let containerElRect: DOMRect | null = null; +let seed: string; +let logs: ReturnType | null = null; const containerEl = shallowRef(); const canvasEl = shallowRef(); @@ -414,22 +431,25 @@ const comboPrev = ref(0); const maxCombo = ref(0); const dropReady = ref(true); const gameMode = ref<'normal' | 'square'>('normal'); -const gameOver = ref(false); +const isGameOver = ref(false); const gameStarted = ref(false); const highScore = ref(null); const showConfig = ref(false); +const replaying = ref(false); const mute = ref(false); const bgmVolume = ref(defaultStore.state.dropAndFusion.bgmVolume); const sfxVolume = ref(defaultStore.state.dropAndFusion.sfxVolume); function onClick(ev: MouseEvent) { if (!containerElRect) return; + if (replaying.value) return; const x = (ev.clientX - containerElRect.left) / viewScale; game.drop(x); } function onTouchend(ev: TouchEvent) { if (!containerElRect) return; + if (replaying.value) return; const x = (ev.changedTouches[0].clientX - containerElRect.left) / viewScale; game.drop(x); } @@ -454,9 +474,18 @@ function hold() { game.hold(); } -function restart() { +async function surrender() { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.areYouSure, + }); + if (canceled) return; + game.surrender(); +} + +function end() { game.dispose(); - gameOver.value = false; + isGameOver.value = false; currentPick.value = null; dropReady.value = true; stock.value = []; @@ -467,6 +496,45 @@ function restart() { gameStarted.value = false; } +function replay() { + replaying.value = true; + game.dispose(); + game = new DropAndFusionGame({ + width: GAME_WIDTH, + height: GAME_HEIGHT, + canvas: canvasEl.value!, + seed: seed, + sfxVolume: mute.value ? 0 : sfxVolume.value, + ...( + gameMode.value === 'normal' ? { + monoDefinitions: NORAML_MONOS, + } : { + monoDefinitions: SQUARE_MONOS, + } + ), + }); + attachGameEvents(); + os.promiseDialog(game.load(), async () => { + game.start(logs!); + }); +} + +function endReplay() { + replaying.value = false; + game.dispose(); +} + +function exportLog() { + if (!logs) return; + const data = JSON.stringify({ + seed: seed, + date: new Date().toISOString(), + logs: logs, + }); + copyToClipboard(data); + os.success(); +} + function attachGameEvents() { game.addListener('changeScore', value => { score.value = value; @@ -492,9 +560,11 @@ function attachGameEvents() { }); game.addListener('dropped', () => { + if (replaying.value) return; + dropReady.value = false; window.setTimeout(() => { - if (!gameOver.value) { + if (!isGameOver.value) { dropReady.value = true; } }, game.DROP_INTERVAL); @@ -511,6 +581,8 @@ function attachGameEvents() { }); game.addListener('monoAdded', (mono) => { + if (replaying.value) return; + // 実績関連 if (mono.level === 10) { claimAchievement('bubbleGameExplodingHead'); @@ -523,9 +595,15 @@ function attachGameEvents() { }); game.addListener('gameOver', () => { + if (replaying.value) { + endReplay(); + return; + } + + logs = game.getLogs(); currentPick.value = null; dropReady.value = false; - gameOver.value = true; + isGameOver.value = true; if (score.value > (highScore.value ?? 0)) { highScore.value = score.value; @@ -551,10 +629,13 @@ async function start() { highScore.value = null; } + seed = Date.now().toString(); + game = new DropAndFusionGame({ width: GAME_WIDTH, height: GAME_HEIGHT, canvas: canvasEl.value!, + seed: seed, sfxVolume: mute.value ? 0 : sfxVolume.value, ...( gameMode.value === 'normal' ? { @@ -690,7 +771,7 @@ useInterval(() => { }, 1000, { immediate: false, afterMounted: true }); onDeactivated(() => { - restart(); + end(); }); definePageMetadata({ @@ -922,6 +1003,28 @@ definePageMetadata({ } } +.replayIndicator { + position: absolute; + z-index: 10; + left: 10px; + bottom: 10px; + padding: 6px 8px; + color: #f00; + background: #0008; + border-radius: 6px; + pointer-events: none; +} + +.replayIndicatorText { + animation: replayIndicator-blink 2s infinite; +} + +@keyframes replayIndicator-blink { + 0% { opacity: 1; } + 50% { opacity: 0; } + 100% { opacity: 1; } +} + @keyframes currentMonoArrow { 0% { transform: translateY(0); } 25% { transform: translateY(-8px); } diff --git a/packages/frontend/src/scripts/drop-and-fusion-engine.ts b/packages/frontend/src/scripts/drop-and-fusion-engine.ts index f71f3a668e..9db93d1534 100644 --- a/packages/frontend/src/scripts/drop-and-fusion-engine.ts +++ b/packages/frontend/src/scripts/drop-and-fusion-engine.ts @@ -5,6 +5,7 @@ import { EventEmitter } from 'eventemitter3'; import * as Matter from 'matter-js'; +import seedrandom from 'seedrandom'; import * as sound from '@/scripts/sound.js'; export type Mono = { @@ -20,6 +21,18 @@ export type Mono = { spriteScale: number; }; +type Log = { + frame: number; + operation: 'drop'; + x: number; +} | { + frame: number; + operation: 'hold'; +} | { + frame: number; + operation: 'surrender'; +}; + export class DropAndFusionGame extends EventEmitter<{ changeScore: (newScore: number) => void; changeCombo: (newCombo: number) => void; @@ -35,18 +48,23 @@ export class DropAndFusionGame extends EventEmitter<{ public readonly DROP_INTERVAL = 500; public readonly PLAYAREA_MARGIN = 25; private STOCK_MAX = 4; + private TICK_DELTA = 1000 / 60; // 60fps private loaded = false; + private frame = 0; private engine: Matter.Engine; private render: Matter.Render; - private runner: Matter.Runner; + private tickRaf: ReturnType | null = null; + private tickCallbackQueue: { frame: number; callback: () => void; }[] = []; private overflowCollider: Matter.Body; private isGameOver = false; - private gameWidth: number; private gameHeight: number; private monoDefinitions: Mono[] = []; private monoTextures: Record = {}; private monoTextureUrls: Record = {}; + private rng: () => number; + private logs: Log[] = []; + private replaying = false; private sfxVolume = 1; @@ -87,13 +105,17 @@ export class DropAndFusionGame extends EventEmitter<{ width: number; height: number; monoDefinitions: Mono[]; + seed: string; sfxVolume?: number; }) { super(); + this.tick = this.tick.bind(this); + this.gameWidth = opts.width; this.gameHeight = opts.height; this.monoDefinitions = opts.monoDefinitions; + this.rng = seedrandom(opts.seed); if (opts.sfxVolume) { this.sfxVolume = opts.sfxVolume; @@ -129,9 +151,6 @@ export class DropAndFusionGame extends EventEmitter<{ Matter.Render.run(this.render); - this.runner = Matter.Runner.create(); - Matter.Runner.run(this.runner, this.engine); - this.engine.world.bodies = []; //#region walls @@ -223,9 +242,12 @@ export class DropAndFusionGame extends EventEmitter<{ Matter.Composite.add(this.engine.world, body); // 連鎖してfusionした場合の分かりやすさのため少し間を置いてからfusion対象になるようにする - window.setTimeout(() => { - this.activeBodyIds.push(body.id); - }, 100); + this.tickCallbackQueue.push({ + frame: this.frame + 6, + callback: () => { + this.activeBodyIds.push(body.id); + }, + }); const comboBonus = 1 + ((this.combo - 1) / 5); const additionalScore = Math.round(currentMono.score * comboBonus); @@ -244,7 +266,7 @@ export class DropAndFusionGame extends EventEmitter<{ } else { //const VELOCITY = 30; //for (let i = 0; i < 10; i++) { - // const body = createBody(FRUITS.find(x => x.level === (1 + Math.floor(Math.random() * 3)))!, x + ((Math.random() * VELOCITY) - (VELOCITY / 2)), y + ((Math.random() * VELOCITY) - (VELOCITY / 2))); + // const body = createBody(FRUITS.find(x => x.level === (1 + Math.floor(this.rng() * 3)))!, x + ((this.rng() * VELOCITY) - (VELOCITY / 2)), y + ((this.rng() * VELOCITY) - (VELOCITY / 2))); // Matter.Composite.add(world, body); // bodies.push(body); //} @@ -255,10 +277,25 @@ export class DropAndFusionGame extends EventEmitter<{ } } + public surrender() { + this.logs.push({ + frame: this.frame, + operation: 'surrender', + }); + + this.gameOver(); + } + private gameOver() { this.isGameOver = true; - Matter.Runner.stop(this.runner); + if (this.tickRaf) window.cancelAnimationFrame(this.tickRaf); + this.tickRaf = null; this.emit('gameOver'); + + // TODO: 効果音再生はコンポーネント側の責務なので移動する + sound.playUrl('/client-assets/drop-and-fusion/gameover.mp3', { + volume: this.sfxVolume, + }); } /** テクスチャをすべてキャッシュする */ @@ -292,13 +329,14 @@ export class DropAndFusionGame extends EventEmitter<{ return Promise.all(this.monoDefinitions.map(x => loadSingleMonoTexture(x, this))); } - public start() { + public start(logs?: Log[]) { if (!this.loaded) throw new Error('game is not loaded yet'); + if (logs) this.replaying = true; for (let i = 0; i < this.STOCK_MAX; i++) { this.stock.push({ - id: Math.random().toString(), - mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)], + id: this.rng().toString(), + mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)], }); } this.emit('changeStock', this.stock); @@ -327,10 +365,13 @@ export class DropAndFusionGame extends EventEmitter<{ this.fusion(bodyA, bodyB); } else { fusionReservedPairs.push({ bodyA, bodyB }); - window.setTimeout(() => { - fusionReservedPairs = fusionReservedPairs.filter(x => x.bodyA.id !== bodyA.id && x.bodyB.id !== bodyB.id); - this.fusion(bodyA, bodyB); - }, 100); + this.tickCallbackQueue.push({ + frame: this.frame + 6, + callback: () => { + fusionReservedPairs = fusionReservedPairs.filter(x => x.bodyA.id !== bodyA.id && x.bodyB.id !== bodyB.id); + this.fusion(bodyA, bodyB); + }, + }); } } else { const energy = pairs.collision.depth; @@ -354,6 +395,69 @@ export class DropAndFusionGame extends EventEmitter<{ this.combo = 0; } }, 500); + + if (logs) { + const playTick = () => { + this.frame++; + const log = logs.find(x => x.frame === this.frame - 1); + if (log) { + switch (log.operation) { + case 'drop': { + this.drop(log.x); + break; + } + case 'hold': { + this.hold(); + break; + } + case 'surrender': { + this.surrender(); + break; + } + default: + break; + } + } + this.tickCallbackQueue = this.tickCallbackQueue.filter(x => { + if (x.frame === this.frame) { + x.callback(); + return false; + } else { + return true; + } + }); + + Matter.Engine.update(this.engine, this.TICK_DELTA); + + if (!this.isGameOver) { + this.tickRaf = window.requestAnimationFrame(playTick); + } + }; + + playTick(); + } else { + this.tick(); + } + } + + public getLogs() { + return this.logs; + } + + private tick() { + this.frame++; + this.tickCallbackQueue = this.tickCallbackQueue.filter(x => { + if (x.frame === this.frame) { + x.callback(); + return false; + } else { + return true; + } + }); + Matter.Engine.update(this.engine, this.TICK_DELTA); + if (!this.isGameOver) { + this.tickRaf = window.requestAnimationFrame(this.tick); + } } public async load() { @@ -387,17 +491,22 @@ export class DropAndFusionGame extends EventEmitter<{ public drop(_x: number) { if (this.isGameOver) return; - if (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL) return; + if (!this.replaying && (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL)) return; const head = this.stock.shift()!; this.stock.push({ - id: Math.random().toString(), - mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)], + id: this.rng().toString(), + mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)], }); this.emit('changeStock', this.stock); - const x = Math.min(this.gameWidth - this.PLAYAREA_MARGIN - (head.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (head.mono.size / 2), _x)); + const x = Math.min(this.gameWidth - this.PLAYAREA_MARGIN - (head.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (head.mono.size / 2), Math.round(_x))); const body = this.createBody(head.mono, x, 50 + head.mono.size / 2); + this.logs.push({ + frame: this.frame, + operation: 'drop', + x, + }); Matter.Composite.add(this.engine.world, body); this.activeBodyIds.push(body.id); this.latestDroppedBodyId = body.id; @@ -416,6 +525,11 @@ export class DropAndFusionGame extends EventEmitter<{ public hold() { if (this.isGameOver) return; + this.logs.push({ + frame: this.frame, + operation: 'hold', + }); + if (this.holding) { const head = this.stock.shift()!; this.stock.unshift(this.holding); @@ -426,8 +540,8 @@ export class DropAndFusionGame extends EventEmitter<{ const head = this.stock.shift()!; this.holding = head; this.stock.push({ - id: Math.random().toString(), - mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)], + id: this.rng().toString(), + mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)], }); this.emit('changeHolding', this.holding); this.emit('changeStock', this.stock); @@ -440,8 +554,9 @@ export class DropAndFusionGame extends EventEmitter<{ public dispose() { if (this.comboIntervalId) window.clearInterval(this.comboIntervalId); + if (this.tickRaf) window.cancelAnimationFrame(this.tickRaf); + this.tickRaf = null; Matter.Render.stop(this.render); - Matter.Runner.stop(this.runner); Matter.World.clear(this.engine.world, false); Matter.Engine.clear(this.engine); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0f74de843..9d98224822 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -787,6 +787,9 @@ importers: sass: specifier: 1.69.5 version: 1.69.5 + seedrandom: + specifier: ^3.0.5 + version: 3.0.5 shiki: specifier: 0.14.7 version: 0.14.7 @@ -7401,7 +7404,7 @@ packages: hasBin: true peerDependencies: '@swc/core': ^1.2.66 - chokidar: ^3.5.1 + chokidar: 3.5.3 peerDependenciesMeta: chokidar: optional: true