From 67db812de4d0095bc539556a2acd3f483be3571d Mon Sep 17 00:00:00 2001 From: wxm Date: Thu, 2 Jul 2026 05:11:24 -0700 Subject: [PATCH] Add combat combo gameplay --- .gitignore | 2 + assets/art/characters/player_punch.png | Bin 0 -> 3228 bytes assets/art/characters/player_punch.png.import | 40 + assets/art/characters/warrior_man_sheet.png | Bin 0 -> 68541 bytes .../characters/warrior_man_sheet.png.import | 40 + assets/art/characters/warrior_woman_sheet.png | Bin 0 -> 65495 bytes .../characters/warrior_woman_sheet.png.import | 40 + assets/art/effects/effect_hp_mp_sheet.png | Bin 0 -> 738 bytes .../art/effects/effect_hp_mp_sheet.png.import | 40 + assets/art/effects/effect_sheet.png | Bin 0 -> 625 bytes assets/art/effects/effect_sheet.png.import | 40 + project.godot | 25 + scenes/characters/character.gd | 35 +- scenes/characters/player.gd | 509 +++++++++- scenes/characters/player.tscn | 871 ++++++++++++++---- scenes/combat/combo_window.gd | 55 ++ scenes/combat/combo_window.gd.uid | 1 + scenes/combat/input_resolver.gd | 151 +++ scenes/combat/input_resolver.gd.uid | 1 + scenes/combat/player_projectile.gd | 42 + scenes/combat/player_projectile.gd.uid | 1 + scenes/main/main.gd | 246 ++++- scenes/main/main.tscn | 360 +++++++- tests/test_combo_hud.gd | 87 ++ tests/test_combo_hud.gd.uid | 1 + tests/test_combo_window.gd | 209 +++++ tests/test_combo_window.gd.uid | 1 + tests/test_player_air_attack.gd | 172 ++++ tests/test_player_air_attack.gd.uid | 1 + tests/test_player_combo_input.gd | 510 ++++++++++ tests/test_player_combo_input.gd.uid | 1 + tests/test_player_scale.gd | 21 +- 32 files changed, 3297 insertions(+), 205 deletions(-) create mode 100644 .gitignore create mode 100644 assets/art/characters/player_punch.png create mode 100644 assets/art/characters/player_punch.png.import create mode 100644 assets/art/characters/warrior_man_sheet.png create mode 100644 assets/art/characters/warrior_man_sheet.png.import create mode 100644 assets/art/characters/warrior_woman_sheet.png create mode 100644 assets/art/characters/warrior_woman_sheet.png.import create mode 100644 assets/art/effects/effect_hp_mp_sheet.png create mode 100644 assets/art/effects/effect_hp_mp_sheet.png.import create mode 100644 assets/art/effects/effect_sheet.png create mode 100644 assets/art/effects/effect_sheet.png.import create mode 100644 scenes/combat/combo_window.gd create mode 100644 scenes/combat/combo_window.gd.uid create mode 100644 scenes/combat/input_resolver.gd create mode 100644 scenes/combat/input_resolver.gd.uid create mode 100644 scenes/combat/player_projectile.gd create mode 100644 scenes/combat/player_projectile.gd.uid create mode 100644 tests/test_combo_hud.gd create mode 100644 tests/test_combo_hud.gd.uid create mode 100644 tests/test_combo_window.gd create mode 100644 tests/test_combo_window.gd.uid create mode 100644 tests/test_player_air_attack.gd create mode 100644 tests/test_player_air_attack.gd.uid create mode 100644 tests/test_player_combo_input.gd create mode 100644 tests/test_player_combo_input.gd.uid diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d05f856 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +.godot/ diff --git a/assets/art/characters/player_punch.png b/assets/art/characters/player_punch.png new file mode 100644 index 0000000000000000000000000000000000000000..c9a466098265a13f5f26383823f223194031eb4e GIT binary patch literal 3228 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 NQ$s6*D!rTc{{!G)IeY*B literal 0 HcmV?d00001 diff --git a/assets/art/characters/player_punch.png.import b/assets/art/characters/player_punch.png.import new file mode 100644 index 0000000..38ad26c --- /dev/null +++ b/assets/art/characters/player_punch.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c3okawgkks4td" +path="res://.godot/imported/player_punch.png-14a58131e3712983546ba83347b7e913.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/characters/player_punch.png" +dest_files=["res://.godot/imported/player_punch.png-14a58131e3712983546ba83347b7e913.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/art/characters/warrior_man_sheet.png b/assets/art/characters/warrior_man_sheet.png new file mode 100644 index 0000000000000000000000000000000000000000..517536a119a31b8bb587021471ec6f9d9021feac GIT binary patch literal 68541 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

(KE zUUye)r!{R=I{nZX&zTzAb9}u#jd*#~^R>)3C5X~jDGPA@Md_RQ{$;XnW*<1Qg|C?_ zP#!EHNTAicKL1VC)8S0xm;BBT^|QHV>wCx_4uR;2xIVuaH2<~eP!QD_4^wqiTKL%& z-z5W~y&;O=^F)X)otQnMs6_ae;&bu^O^|Q1^ zQd=h*MlBnJ9gp7_!p*Ut2IsbPzrC(?jQ(2H`{u z$N++Yni?;3)6pmQH1j&Gt?+7l4}gKtL}dHclq|_cc!9>YONLBz*_%XBO8?TIH;ZRD zqQJq$#*peDcS2f7I`yRBxdbescDz$SiMg3NP}=d7k6IAuWs&$(&1MAjT!&p$2Pmz- zw$8sXX$bu34A~gUSV6c#0Tu~bQ30PTJzr9&py>KF6$@?nsicwcwagGgq8 zo-!lk{r7qv)9eQdty|Y^c+iUJ542}@GkJO2<`4s7Ed{Nv zAlY01NVG>^HD#VPp6lpw4O%<`y7Eu&j2w#RYY;AyufSE!kEyKDqPzU9)M?!8Kjtah zk;dezeIRU?g*fOR zt7vjhFk=M3;GwGjez4c0gEzP z-Sq}Ny)KYigDW`$t-2PzNsNK6j1dYio&d?kZQY{!3+et}$pgkruoEsPS#AeNUV^2% zcS#DJ9Ze>1&&Og^i{V0dRD4O1P6Z9W6?*(^ImvQ)1GXbrmSaSouY)mVWX!3=dw^#e z!4GCil)cFuB=y9qqvu>+{>A8db;l866i6mIkGxM8#{E__x@2j2X|tb5)9_|okKymf zeW_)XX-eu3$o_1!_{MiIwieLt5O#qhbi}OH;`7xN`EeC;2(@Kz`~Z*{D=8RlzyfC{ zje?fD78A74rH*e(k#5a~9mhwr2)bBn*3%l-RhpMn7klLqXi43J*)*UsAz?RWN&@uc zsjA!aHB_k0z}Tpwuf(=(ddC6*-4K}qOW2oeEAk_VHF%lz?Y1#sinc+Nn|^%yR;MUN z^cuvSH6fxS7Q84g7hEh=Rv=XWWgdMYCwS=zv|yiV`n_avm-ku10$IgvW-Me~B>_NIQn?vbImfY6n9J%6=xnUU)M>{Gk{PoG+^GEAde?zr7X0|8Aaj<4(0+1 zb^YxiHSCf6z6E#`*S^b5#2{QA6mLzr@=(tPJ=o}f8`d8G=4l=IoOoIDtBVx#%S*b% zy})qq@$SW-LOb8dEID57GfZiwF3l3y%r>+Wrk!($c?mw4*4Z}-*qZ|-5WkZ#0aKkB zaE1&F5AuPG+s{@hL!G7{+&=?!EUfi9z*KK{x!DziT({waOLt&B1(QD@T*T{Iydtpz zLNBwhU>;GtI7;iQIYO@p$vn*pkYcv8>a9?_396vX)n)IxcO-U77sgCAyl3EZfcSj! zS~B+b9lHZm{@zzFS1m)0_PxI(Su#8%Yn&2mI3EXY;qbC3VYNX_*tNp@+A-+ZQ&p>e zYM+DfAm`n`5D<5&teJMcXK5>Eg()>wQyvIjv*OanQwz-{e1he0ErJWx5%LYH-chdo z0na~4(D2`UkJ#W`><+RgS(pLa7dn`L?t&Pv3*3zXCPp>unC%5^;9csrS>~T$##dlU zuU5tT_YokN*K$@xv@N~RBZ)7R9hGFILZKCKO$v+Ljvfwm>667(OnHGjSSI&ofQhE! z5(09Ee~N^RUh#XVaVDLjIAAL)gO<-9{w{4>BW}q+K6<>9ttvCthog;>r}G11K-QgG z+t!=MNwDpO0T8a+aHHY2dD-r-fJp;3z5YJ`VTiFCzz80JCe&Xm2xyW2@4qBVAicO6 z!j1_`Ldvq!EWjRsql_bvSaZKrjDjlA(2IKykY+|gGB8dk4LU3|WN zWxgDJyqLY~#d!OTT|0k8d;phi{L%lB$Em7v_-|ap<^8a)aFlbaF zCBE;6Z~t|Sp2a(%kpQ3JckuH6!9r%0B=4qxo|&7952G|8RbGGvITeTqR4BxIEy`L7 z6v!#XNGknW4=t7bzZF?y1|W^xw;|`^+wIO*XSlIUH?%8(Zw(m#if+Z{v+2Zur_7Ln z{bc&m<*Ei{0EQ-ur(SU?O1D!AeV{Z|LHq)Z9wmqnX>=LJe?7hb&pRV^*FiY?vdxMo}X`}z5ncS#3s~DEOX~e2rc56N$<;*f_?!f!(TW1 z!uJ4LUq^tr9uTu)xa$b1TstNwBR~P|Jy3To=AVEH+s@P=)Z_JSvHM91Fdl?-5&b>fvk=tnc%Lss;>j zZ;2I7fnz1Bx>X7TirP4d@_P7lTL(wa737(Hf8KVSKKJ6mzC?hQ)5wUW2(yj9jCFZaaFOF zXIc9Yfw%qCK2-(-z#~QQ!pLj>V%n*2I(B^GTu2sr-=Evy{vDQKVG7_JfIZuVjWAo= zS+reiNJXc~nTTH-yT273+nkT{^)O{!2gW5|^LkdJmQ3a1+_d?K{4A71Eq8XlwM36` z@Jasx^&G%oUbB%`6YM!!iaK|8{&sk8qb%pSeTnT;19e`{jR?y&z(5GRH`2kte=d40 zov@{&fI>P=XoI87j9ViPKX9%Ja6c}6d{l^aJUq4NS)* zfig6xiIBEcCXX^SzQ)Qtw(5zeF>b7CDyUD)%o(LGbgnf~0kHqoU6Zpx((~}w7kcTV zbMYZg@-Ua@R%YKLK6HvUj=cb9b2=@J11mCqM4RpS8s!a91rHpooB zZ@c)!XxK2*7~4zZ-W>1g$1&q7f}^MNgJxm|Vt~yZJa^JheR&V0UAK)2xvG@|J-G@{ zP+s%p>suq>N-MMc+?}2-n^9)#en5f*euR2|bcW#Ug$Rnv#2?<_??3Whr6%8}nR<`bY)oc!tgBO=Z<`tgtIC#}Qo5}Gb={bJ znytHPu8}s|U(pp5VqNKFfo({paWI%Ok_yl+_J}Jehe3m!on?utW?T!41ZK@_r3XRLUPRkY|3%Ra__vPLa!YHbtJKbsUu^_P&y zmvnn2+@pQpT;=3KD7hH)*=u0?hOZ&jVX3l4L89CCw0V1c^+mxIl>}wnC2`ywa7)1X zqenqlIz*_`CR%hDE*0QwM)@7}>lq=m$rhrFH)(k)iiA#GLa9Jz!7d>9wveR35OXl% z-6rw2Rabxpsegkv?DhPk`S{P;DA@(K`knGk{zbl`tZD-`v29AR`9emyLr^KhrgC%v z2aY&+*nyVe?4@zdxwU6^AwOyNPMfnhjB?08)vs7D8|6Pzkb%n?-Eynnhzf%Y2C~H@ z#gb*Kj4oi!Pv3yXOA_#Oc*ZmIXCQQ!-8`Bgrk;XG+-N*({Z?KBrRr*unU0=#FQ676 z=^X!{4f+_A%_;zs2?J^(2=QA6b?gSAv;!=>;&O71b4_Gh?5*8%nY<>CvcZOu%p-3* z;)t2EzFR;;AjlDYVfzY!$rSB*357A(In#cX@&!%%cpdf|8`~8}qoo(oAYW-ZF$t9i zfWvDsKy0{EKLZRx+|tU=5b{`#hN$<*m>IUp{8q@gt$x2RIypMScL@vl-X24&H8IzX z?={~8&ck~(cUT>I3qb;iK}G@AcN&P;4jrZiNd(<#Z{zYatO%9q0V~v_e$Kb8ms0Pl>HdV;;&G82BBW`P+fWbg6o`Q<1`PhL}rAjk0 zO#M`^@<3ap<#LX2S&%IKL+{GeA1wSO3B=8IST+N2{q}{Gjd_u#o(=4QSAkXq#1qXeImY?-ghM-1DOP9wO4(OO>@Ybfs zqp{Fl3M_TxGOF(dH4s=7@hu%8l`DyMGMVX^y#lW(*;XjCKX>}POnMRWY}{)mVTTke z9*kP)lHqBSss8yjj8Bb87xGuEHg5*b005|>%f_P@*lLFS-vI8bCz%L&6d?)qmcRlp zt6V|hbNOC@j}%cUT6WpR-*YdvR+nSW+-omz`&HJmkiG_R!PEs-yabMNVUfdmYxoQf zg>KO!)PW`hJ5e~!)~syMws5%B0-AoeN-PXD`EthS?H(zR7#SxRm0^cEy0B($fId$4 zrDb0^c|#u8Z>AOU42`2@hjGF2ufd!3QFUgybO6<_FqmYAEyZv`-Q|i5T)|M%FW@#B zFEbxGHcrPv2n|EkWTAloS$Ak}fm{Sf+;(8rE@SYokgZ=PK+mvB6FI`h_HLV65{I^j ztXb-iErA!O5CNCm#^Gjj^lj4_Yc}wb$r)(D0M_$&vkJJ|Vm5#vvT=<0|8WOJL;KdM zcm@WuA`3**p%g&m0YnRO&n&iGY{{M0+r((T*1n5uHb+E_SmXjtS>WG>QQS^D4*8djB_RCFJJe~b+dveaLFdi3qJbiIn*h}9TnZmS z@NqV{l!|d)grBREkcIpNO;=}HGLdo5hBJZ^VZK1 z&#m&&sx+v&XbK`Vp0&XOx5uirV&LfYJW})&sLhf99Da?`hW=a+u=y7kzj2|Q8*Dc6 z=7zg71rwR2TtTt%T+@ms2K9j6y6#Bo3qYXU0Xd97Q)rtN3Jf{g>PsK7W_89|&GbOl z1a>VABv(1B7$;5tvh^srm|AdIEvV`AZ=Os1PMv3VklCd(^i0_N&hBO_xh_^x=bX)K ztUK##(+-zuV)QrGDrMJBRsaUmJ3WE;0UBfhJ<6WX5bNgHK+@n4W!_LG>vvQnn zw(CG2m^F!gZLkyUBLqdN4sM8B_QM4*f3GQ7dYZrt%ij_D2{e=SA~s@6qF55FaHZ^W z78r@nf6C-;j)T(LGck}!#hO;s@drI%9co3#hoaziGb~bNxK;|+1nF%RV43-eUj4ENf2 zGw)Ejck{wwgXOAQs0C2Ds|-WjclG_`@rJzdUn|SDvtM<+rv8ln$8H66Mw3+zs)8-a zXV~n-J)Z)HUAL}@$9(QCotV)j!f?=nD!RsR_K0T#jM;siuW&yPWat0zF|yNFP7jBUod^oX}TP3obuo?rWX_l}rDsr`U@ z&TR{U>7Khm(6yAZC8mvLyo2tc2iNn5pdbZuT>B1FDM&3|OSjSzr{zVk)T1{tvc0ewIj_vVuTCI}m;K8179t3ig* zH;cCDtTKNK6obkv_{l-6LXHv~;OiIa{q$p|qFLVx@WMr@laN6nLTb{y%v4S(VkoK6 z9wbV>Bol#K+j-KcOY-}2<3p;QE-9_$fH(T@z9oJ1D(Ua+$TieQyhw(#2t<#eWy=Qz zv|uuZsRF26KZwfh`~!0dw39SWZInq-M~*Rv4?>gs_OtKs9^!Mv{&v!h6=uO=4i23{ zKMSx1WquT-U{jEtptx2FGTsuH3;n(P)5LjVs@EcJ-=?b2bdW<|QR;rr+^%WnyuKP@ z>0aXMiM!HtD8r*J`(nz%Xel*|Qo8mjKXz0(?-na57~WYVUkYrUu~FCmEW{@uVpM!5 zNX7vN2A=rRrpW_^E4K>*mICH7k}HTbG(ABB6--N;T{2~ztz=B2)BP4lMR5D_(c8q% z>SA%79O91=z_n3;WkU?IPvp+CjBYm=xBb^u{F}e8_QM<7sftfIn$vZl@z7Pd{Lsg4 zdF6l-P_LSXC08hd?21~R*1*S!-|2?OC>U=NgU^|v$3mav%(Sa=-Uo6%mQn+R{kE|x z6X)p{w>kRZEnRwx8{L$7g9|v!Oi9pp4AT;I*mT=h4+R0nnRXmhSu$EacD&HXgd_zh zXgDsw#DkHhpT8npcm|q807XoH;fiF9vC&LhL2T? z32T@=kp2Mvy}2NQ`-TCz@Kq{9sBnQdDbIt$<}PL8cFS~A816;Bx*u@XovP`#O0#c? zkRrK2Y{+#|T>xn8Yx4`ZFN%#BHLJve-dC6ah@24{V@|$ae#2O9QBNSc3p6e@-nK>4 zuOjjMU|YqH@R~Ohq5p)Gw*51-ZEMQAf>{HJubs`Dj!Rb%-jtU?-;5EN>#tN|C%43f zbn7>bbVmR30<7Etu8(1jKc!3K_m-he1;ZK&4sVC^7Zt$+=i4O+xJ4CXW%HL-(Pc5T zf(T&djU#&9Ed!2V0Ha0*z{c4&Xd~J}`wgKF!^#$9$oQ>eP5T`?BH;_ves|o);_JK2 z1jh@l0uZ$W?f7N_La0y0c`aHUbyhAUng(g&5bpZHixR&8mrR1gVD?|+>Ux5cbiS9d z8t@|nKegj;9|RSNMyy`djAta3LvQ;eeNZ8&jnY*Nnp_@yEt#M`v%b9}4ESTF7Ng~b z2xW_NBLQ8mRX&ind8>WJRKj}ObVFops4FaGEdTJET#)GRUE#+Q;5mMf^BA%G-8h!w zR^|(?YhB^@C&0Q!#)6m9UoX!Hu8mY1+0S^+F}EGXf0rBKGaY;+3SMvE0U4!K;X9SLVXU*6U_XoxWEIz8(sT1psnZJ4DPVW}vtZnYP0Tl& z8JT5D@l3TJ)ags;DG|ukcnS_B{^Hlx3T{M~-QEqnOH1{t1i7Kp1!^1gsB5{2kooWL zyt_2#kwk?9sW5IoQvP(klzvQ($HuLgDer+a<@}0N#NA$Imqdj;i+o*@YW_Eyg#i4S ze@B1Cj4FP0sr~hHp|t<>%*1Tze*A|Bip{ILhwz|TAhs%Mhb5&L&N|P_Vl9jYKeJr* z7vFT+Yg&7%m~B2{qxrX_amTboooGo-W6eR1MvCtxjYGTF;tpOa(oOnUe*^Vd`98NJ zXNi}mROG?arr2VS%e%L3?zM-(%-!A}qqRp^J07yF36d|-Fb(mLbZwA1abksGw^`b? za(MtRS|_sj2Cs&?&U z`n4tRu`|q0@Odq`%(_NvM%+4v7OHYvi5W9lQWhQaCY0A10+N29XnAoYDr1qJL@q$G0@D9^L+ncmi|v zvxKMhR+#IAdf}kpfdj;X^T+C`ix*TiU71a`cQX6m`O%hi+P`~}c;)%JUN21SomPyM z$1*yQ>r9U>^y41h&O8M}2gT&kkdURvS**FCjX8GW6Y%TEhpVKfX~GhDgSlRW*g5ic zx#BhEP&Ztcu~h zwT?fv3S0y2Nyj1|x5^vvTHdwu-x|Ojy{b-)Hw{?%W_J9!{F~%EO#Cgv*y{M8!f(YM zjW0Pq-0@X>AtGE~;<6I2iN5K4N<*XU;byrgtYwezoLcIuy$J`Kz;C0fQK#s= zA3WC;tYS@1>GlnH&Vytx!d2W#f4i(WoB1|B-&;!yBO^3~gE?lXD89Pc(5dF{v2s<$ zr0dN;JLIqanU)+S94&wHuARJhM_*Poh7P0=}(PPJ-VAQ_jO&RNm2qU3Qx2vv+js0v3 zOHy%Rp?mT$7A|z7sXZg{1u0kchoWv?kg!Qi{C~Qu6%)*;%ci_~6e)6+&yl97(dy~d z$3Hx9{v}q-W$IY2KP`-+sZmRw`z#J))u81z119;AeCzO}r2g87qk(N-U!1?;+O?U| zhR+LMp)1@1Cs6NAoyFtA*QgQ!B6(?K;)ko=>t^{Y0VT(89H6?EvZPkIUr==$0@l-2 zZt@UVe-7u^YI3rAwgR=M=e_vf5W*10sWn4Usv^SaL5x8@^+a>g|IXnbQ<_FkPY(`Q zWzbJan|I2U7RswIp(CL3O{xW=o{fgs6Io29w=276mDv_L@Ry>G}6kEd$`IU&-_;!EP zo4PyL#pbpS>(RM?UhID^QW-5xrUbkB@zV=|y!E0yg`N9@%a@}nI$LMJHSxS<+<&&p z|2i>PO?>Rv&ByT5^^hxjs60(GvUg&Ru0`t`!!YUjNy>tPh4kye>~_b@XmZ1jU)IVD zP-Lbc)u~QX8uxPi{pnTi&bJG%nixwgT9;oln)x|;B5!|?+@dw^jfmZBk@~LJhw;98 zf_3@&C}-f?{?9imOt+5y>e7|W(YvOEy#lLRn;-lh`chrB>Ns5LAIcw0K^rC^)|}3} z>}p5e*1Tgc*hNOTqvQ;*Tf`vW>=D%+@Xxyz-f`HZPk?RL|B6z7{V1?VpHuDg6>}EF zt?|E=%ergcJ>HHfKcHkNxJY;O5|QAUT*I8+dTmVUNPdv!dFv`OPq(sgX`^~zf|Tap znHP)7dHesC8|9qb2D2Z$fu%j-npYm71(Qa-11PN0x=gD%Pf~^5`VD<%MmcY(Hi({N zM6&T3&cJzR{3ge~o)h4NlmBgB;o;%Tv71#OGA*un>6rX$2?G{A<=Jm~u#51LodGS2 zFVpp49rvC)l;0VjSdii?zxb~X-@|rMG0~X&qUOE8O)$eO9S_L!fsXyx196Rw55Iyr zC1-$Atl#LM@Dc<5uoSnUm+AXneCOQ5c1xo|JcID63VXYwKEzH96C{S?_;&{xc5gob zT4!A|79QlvN*#L;@$G!g+Co&1UYd}_n&1;nZzoNSTj)Z3%<_Le+X6m|pCNITZ$A0! z^XI|mUxw?&cOwVV>*Cgh_253J9A0;@7B|6f5;p=a@uz%P09{%`VWu5m-F8Mwm+IxsFOP zwmt?9?-!tzVTKm$cc0IjhwUg--B&u^b`jO%%3*B{{Z*X6UL6XrPGx>#4`15{o^mnXt(k zbdXW4-{fW}tu4Q}t)Ez-`duUF#AOt>&I^RfcMVC_L`L(|iu6w}v*%H3Le6v3exIAF zIvxy+4m`|Gwm#PPux!sC$a)MjxF|yHjE6WbQo3GY%!0Q5lppy55lNFWquQ)GP3oh_ zE(&n*_iW^E!%tOjC; z%EKx?#Beb4#4?DV{fN9d=xmM zF0z^5K6Hi1n9OT-u>8n=ilW`)ff@>W{Y`&|9)77++TG7t6uyPXw6dxie=C)Vjj~4V z1a~y_Z6>(UT~^lP+x;*IL)F`*pelD~WH3*Y8n`VPxk~fHaJy9^`b^;kzick=sf0Jy zC}iN*$1>cKH2MD1xbX1xH9?^c<1`_56ZBaRCK=6d;6I#6=SjN57`13yu4#msh*weg zT}#M_{`)8-arrJ&orUWs9*d$7XzThWEtcTwv$FTH&m|kf!vUIFNbD|5WklZH6Z5n_ za3Sdhjl=ksY}cWa^N!RGovlbDoJ`JZ)(wo9C}A>5!7Ho;gOfd)!<06D4(Oxty`!*cp*=B zygdmUwnEG23lFzSdpYT8Y2+*4T^W0xX5wppm6ClMAW=X;D_UNU_mu%gcS`vLet++zUn_L?amC?jYZD)@?dMKh*-My7F31FHpR5Gv> zS6Lh_A?@ID!$G@aFtGa2s)DL%bhTQSp_hSd#uN$jknHDLd|D2SPEzZA!!#mPPEs9= zS#oZFg0jFQ=6AgMJaX%1386Uft$4rYD)n>K_mE=e>Flt;;)i^hliUq$`@TVGG1F%j zKDn}+%ES0lC1FRd-nwn%aK)ro^>UfLr*W?wjG`f6rdvZ} zER1+h)KqEz5&15Sj>_>OR!?bSs<3>EW9G!{}pCmQ{H3(W@h7}JFP0Mx8G-#%BvKP$XVJ}MB89=B}6Ch{_ z_q&IO;|btQN9It)$1x=FR|9|hpwH3w-Kv-=#ok-k<>wvJPeJPL(CHLKiL89D_v$jr z*&M&*zF*}40}411zRg6K{IBnZ+mPfx)IaUYFJfqe6-xXgxD~r^^O3@w&AhK1VuWAK zO1mmYOZr$APlTy6)9+6vAEtCT%Y(piaB&lrdIA@&FhI;&s)y-S&ToAtTErketif0- z&96#Wzl-4R9n^hb7bA8r@q0V$IcQx>a0$C|Hq=K74-RDU*Iy9X-x3*@0OJ`ufE3)Y zU!4^8*hSs1Lu+;5y_$>6449d!47{ongE&x@jOeFx(lkrq%tF32Hk@hFE?%?t&8=p6 z3E?THt%Pm#R+z-)n)3EX4y~0jtEzn9=?8_I(+M-4GbVqAJvfRc#|nOO7IbA(FuG{+ z^gfiCzoN8mK%n65q0dJaJyr5V;bT>3c4BLZNKMU$kX#|DWhyNiUAho9>?9ev~p7qf7a}6 z@4AAT{0~PVA8WhH)t)Ri2eQpP;u)8x6m9QQl$16nwqt0rYc5S3{`H*l1b2g@-|>M} zK_QtExmR6=(VKQFv%eoWpx<14brbVR2EV8>Ws>oC0k`uBEA3T;hKg0rV{?Y}4sVs4 zw8Ox+LhlpDt(DcX2EDaaRJT_44($kgf54rsyqdZE+YY-|Et-KhWAhz3;k^8A`K@M@i-Bpq9hCWHh6$N<;`FWRb{i2bNcr^D0LVJU>r+ zmO&pr`H!UUUaW&D40q>~(&^ivuG_#DQL`2OHWu-@gq z`D4L1u6tkoGNqXQpZa0>+A(TBKy6t!ll`06)zTenA7IuBCh218bnx%8|)m zelK7OPAkU$=i?ISZbwSTAk%)%H>kwzH`{ZRgUs2w6LN z>H&tnkYJ&q`&2d+P#$LJo>rvGAnVJSW^0fne7ew6vg56%o_OOuS+@)hoh{0cR5Dxw zfOVxAYP*xH+eOrDJ`SYLT}(fhJ0DBC20>24ri>hxXFJ?AYoDkWO73}ZtpZ?rC2}vV z*n#-o^t_+3hQ=ysScMPZGb|_+LV1gL@0bi(W^Gs;{$SDbRt|6G;j%~G1)7|^CXvVH zVr`ZA-Vn;1kQ#-vG1I0n{kaaO`>b`e0F-cS;aHP;u>R7MxHYKgnBXs zZtmV(hphp%E#jK<$JamFp(`bxyWxhC{x49eUC$Q{n_wQ6{he#XrT2oj*u7^4_boZ$ zELJ#-f20E%8l!X%>j1rc<>k2WaHT~~1c>gsjaLpFfaqq<3u~9zeXb8Ob*29Gs$>Q3 z247Amp69m82P@Z{ZOSm#?DOqIxNrz}fV8b|Nvd+9*|gs65r?s$q7^HlFGJBS=#>Xb zk+?o->N6qX;~n`&Va3ab%A#(pFGDOnsi?%6+zbNLow8C9nOdMTLGJao$n|3Ata)3J zF?m^|8iqATIG81 z)t05TXth)1Pl9i`72!ZIM^PIE9UM6 zH^YeyGP8?W?im)pzcx^>=%#aLZs|mF*cdB0UG1Dd+@<4I($-?8Ex}h9>2-j!)-e&P zw+pe7qdy1tTcmuEx$oD{fg%%g;o+u1b1&C%C3D29tpz8!d+(K_ofptOcQVWM86#>` z#RrRWfMD=1OQ^HLtyUf#0@rzS#ya+;1lbpwvu;Wv6gH_82Z=*6+=&fk`s@yho zVYr#@)ZaX2di>?Xz8Fc|Lx1t6uUYHhmwFNM2bQnm7?!K>l9W^qm%!}-^XBtK_l!+{ zmqgr-dtuI>*(@QN7Md@WBv^ATn$bTwk)KU^QeV{L{&s9iMB+B|ZgA^{l^Cwl$mV&HmFo3>NGd^Fub0r=T#FUfSSm>U-!XnQX zf6^#$HbV)SpIiErvb!$F2oAWLax@vs(#?7}thVYM*hyroHW^UZMrH-PbQNJ?NVotU z@3$C0|HNDJm{5HhPEn|Gqugcw;eR0R1I$Yw7qnVv_-+YGb=}?_k`(|u>m7gMOH`g0 zN5B5q`n2hmIfO4y)QH*BnsO91+{TVn6r!Xdq_@CW<4&_Kbw1gF91RU0tOaL>L%-G- zvB~(n3*4SeMjV4eMCKbXxSGM>s_`L*Di9t9kvnlu|JFcS+%S40!aRq&K$P=Bh1J;O zL-r-5LZl3nbX;{sOSEwhk+SSkuf3_2vCCwR|0uP_OCW=Y9Q6fD0DUSL@pSLO%C;wM z7}+&zfm(1I;F$9Fw!M6MzyD3kH8T~hekSf4`0S-8Xgb~Lw$0eLg8d~Nb04ni$Jnxy z`@1!}dY~c-_Ra22)IpJsrBpOUhWOozB+;CRH&yW}XW=}}?@Vg#`K&jGt%^Mu22e1B z$QdXsgFp<_frTkyzqkMz11i*eL!?al~YJ6ZmAx%vI>Nw zs2>Qp8zE4e6|fQnF=Y$AcXOTz-CXJn(c*JlM^yzflEcGY4uJLHnbYE12L=DUgp=I2 z#OjlzEEK^NPz$jnt;AjWdjDvr3iqgEZy`)=(AMSt>zv__m{0>THOBQ1x~4cuZaKCj z0}+$TkGo={B`Y93?`iVoH*Ym{-uNygHI%*arKChdBCBbPFM2h^#b*Rl z-);R9LPD$`2rj+-5BRuy+j28{pb1}5=*G4NJ`KPwBG}48X}#s!LAR(;}Vry4;cxCX$boIgLhY4$1=UanJ{u-W`pEytF03@@-;#_rL1fchWV75tcm;;<2ezw6=r^m|>_)v_*? znZ7xO-qCr0bj9&ns3`eZgx1slCKe#y+q@7(1NUwJ@BBLF4N+Q9EXhd-nK(D|1;VI3 zkl*w4A6f7^wlgKy2ta(pR)MM&;Koz^^*y)@avp z0X1-Pu0#FE0U7CpS2O1i%me%Cjq_K10(K08JFNlsbsZsrVa}6qFDhX=K4ZfXGc8}7 zE8uNrmv^UxYl+dU>LVv`?6^SB;C$|BlPR1MGgj;N5~QhTwK^4QiZ+62Hdwj3HijHI zXo6rC16AwER$7oq5qso_PcmQF!84SNjzKi&bWJ&T*p8>|rxr^MNiFfgd-4ueTLu19 z1PUaksqJFbL)}1OSXi0Rc~A$%30ur5q9Q^Y$ASQEq1(81@(T+3I$)!$JG|( z*Xz0XXphP?U zY|K$mKaLp*^48#-FA%s|2mbTGFNFGq{B~g{XBNoP4mFj!vN3rlXz zQI|h8%xpBiZ#p1@+G!H#@A@1``9IXXc|4SDA3i)%X_q7^az~+T$yS7Jk;)dbWf>8& zlby_Hktv~)eGA#Qklh%SB?)2d%aq+%#x}<6zw=T(&;2~V_pkT;=Y4!Wy1Qep>%7k8 z`#rwLah%tglDb}hf33Z@tRIMj6ZcoPcRgzv0WWU63jIDO!!qm3{@C69!+SNw3oF&z zrBZ(ahz>QTb+}%@{@&KTT0bN-LemjEL5YK(h>t5$j@&3A1ia5S@eXZ2q- z!R{9Gt`1A5H+Rs}@G_pOAMY2myI+Ur{;VjD-Y2d8%{`s2p~wT)jaRLM^s705yJIEI z^n%a;yn0M6z=2S})_FliK){Iqf&>J_dDa5S&&&=lbQcChjgdPG!r4K^*mTWw zto*sTca_~RF)gqYIZhEOMj)jCK+az!gMv+wNO(RFm-G9O#Q_9NFIz;|J*m$E4nD)& z=bf)T1uF%d%_R-XIPcJAev!(phd*vXgU~Z`AgqP1(K)EJ7IiV#ya@h4QN z$KFarMrj4KO)%Hv-O{BPwIG0~oEjpt8&QbEe3}KD#R56B$-! z)YlBxfB%q|A5GWz0)kuI!>o3mU?`w9)`&<(cC}UMFxQQ(0WJlWhv|N04qzKPerH^+ z!&I{~-o5*HhmXU(QYr4}g~>jXjD5I~Rd-(M3b*wPaHrg}S>Vbfb9{@XFg~TD`Y9pgKXVGn(u8I{pa=g@unjXRoNK8U z&-AG^^3yjwdpotq@J`r_Bp7y7%nn}d&^(SEJpbs3l6yP>0)iQaku+j~&$O&M$ za~pf?ncJMQj^}BK_I+2ix+;pVnGS6QIJv@O!M{x~D`p&97+aV?y6nKyccr;7IP^A?-=91z|0G_UiM2%gG|2ooOLN zKUGfKUje5{FWEA<^_H+(Vx>9&>JXy|aH6qFr~KigqADx&)@=Dk2-N!lQxRT;gJZ3; z{xp!OLonZb^`lE-{Q>*1!f2W?gp+Q)ng603CIHvBh=OAaYRL#ne!pGKh|9%ENjRjXk~LXz~VdtF?hVw(%lS2I*@*xEN> z0WB?H4d@QkjkVKfd8XGA3$91Y*6iu|mKTKs(Pot9C@C+AkB^#=Ir?~T!);WF8Tf{B zyLMlddwgH(ckr4!n8<5T+!eSmY{T;)pn08lKUPN9DUnS^PNBn9MDli(-w5@LO|eLO z>{T*x9ek1GIwafd)03WK)smg5v9bny=*>Uk5Ip7U0y?X^5XGkpamvla${42c7fD>%ff%Bm5*-C zJS8IX=pfG)h-`HEk%4kbvtFI*kd~UoRj_|Fj97 zDJOsa7z8j@y@OgJ{RuB#C6{E{Xv&~vz7jTm#wL@yp>paM6A9uBem?K%&Be(@@S}W{ zez{`EXdoTwFAMPQ1G-7OIQewcAITnMJefXkdzf8PNz6aha&Q$J@5L*Zi{=&U10=Is zlYkFI8(Asx548Ee`_3QmaDTp}jX~u%0%##$aD)oXO5vsoD@?;TfF&WCOB_J3j!*1r!9}fnn*!@5FF~sj(csKS6_uypV ztW!i8JM3O4T_X@HX>f(U4>qX{*ca?!De$?%I&P1l+%-k?J&V>?IIj+6*jv60{zU_f zmN>Zn%Xkooa}G;!a70>s_&^yWgM$}7p2oXI&?-6y*50Ww^EYBVJxf?t`bMq4I#bslv}7sU9><7Q4Mgjc!g zB+!%+kF+NnqG^R===Vqen8X%ThO!aY`WP8WmfbJ*| z!9y>ou)63pcQHc+I3-ob?rI>An>29ac@HVjh%WxqgHtkr8z0*>J9+pLu~XfZzso?U zNVfi7yul!UOF23&?A|C98dgy@V9+#bgc+p}(Ts|22BDozPr69S)|# zJtO0Vbx0Xpgfp$xrdA`$=a<4DHWCU@Q*B0#ZW2NEN%dBiL@EHC=hOA;e!)qT(&>+% z#yIWL#|Z#f6dvh{@7m_BPV|l;aahbsyt1vtUrAMx$wi@9Bgf3z#gXeYN?`^m4s`c;O#Q48i+%_ zz%-$C`Tj8rijk-y^)z@jRQPuC>DM6GmUsNS7c^QaFU<8+J-Ww`57~j&1MMZl9UD_qXJfN zH;xMl$+K!xBFkA@1S`7{Qz=S1Nu)N%Q>g8({a`pgLM`R%+u=!a0`QB20N~y)DF! zpcKNN6g)e)BMD~zJI-CAMtb36b2C9&80vkM4eLK52|U`UqvBSj)_2!&@^5Cu%z5?B zSo$oSz|Tc?l^uDWZuyg!0~8skEe>|9do$Lv$#YW3k4&>$?b$WlUNkny_DYfUq%?EE zZ2b8QIJYsWE2^?CfaIb8mN}7NV|bBu8t&8)9FL(?qeg;IrMun18JF+z=>#0`}EiFEK8PTZ5_KCPyNY(qCw0aNiZ>?@xybn*`)K%k36Xx z%QO#Xyod)fdDbb6LikZK50bdGu6R%2?O}iQa*VE2>es1h0~3RQT^9E_Q=P82?yggv zrXSorl7Q?i;5%nje=rGv!lQl;>)i?QFQ;zV$L6-5i_%tI<*A_H@N(ty2A!)fP`e4k zx2X15Id(_Fl{c{%n!AEMcc>s{C&}Y0cp;)n#>NSgt{>+=y@ZddWS}OtqR!MZl+O)* zq82yJ#4T4b#_t4#0?*oHC4Z{k=xm$e=H%TU;gB-*^NWNEOKV%?=P#p@qSB-Hzsc`k z-T(ND_PBs`cOblJd;8-Bu{h^T2$#wJJWQ1KxOIZgBX&)MY`ZAA{3X1d$K)hX^4(Oo zdOtK2%)_{|8j~l27mrBlo#AzhTv7j_6~omowIeD=4D&^M#8D;ET*?oUk+mj{JL-D- zaOjP_4lI`Yj7MhJ3CJhk|CU<9A-n%^PODcpRnFEiflW`iR@9z3{+&mvg*O1BkS6Mf zfHqf5b_%?PuaRwIGdo@Zr{uPOPsXfnj=)`CehJ>?x9Zd$3pSM7>38{7F%EFrUNCq* z`?;i!EWKZ-{oGaI`#on`hNR`v?Y42tR!cna8mG$fQ5K$p1IfowQrb_xSbUA1iVw#u ztfkLzNAEQ1yZjQaI8l59(F`}(T6qW90pR#w{o*|X(RN@7jLc2NMcf5$(|_1mfocqd z(UY*l{av7JGO|@~`)_8Dbr5)Re?x-)`hMm*-&9Cs%3-}I90j-8d%!5NbNi%ND z(XN%s%{oZ)_8?C2hlxwDreFb~Ink+0A4QE&0;$)9n;fbMRs2>q704|o0IUJfcUPCL zP5+ZC%wmV`w@3!5hbI=}^N2R|Cix@SF;GrkRGfG*Kpfi5XPGzmc@x-1z-}brD=?7? z2hU%U3XpjX;VKX%IljmrZps}q{=5?TA)~Et@osEA)s6)A*PN!-1WjZ&@P&G zy9`RSB+CmXLE@M`tq%+arF!P8+aWTo9=#cAvl{3?4)$IjK4Nn4bdD=F<`SM zuX1tBr;B|qDW%nuK7B+*>AZx8sw;g{mSr9pB&>AN^P6R}*iQ%@dyUJ4d#}6u*Rr1OK%Ptv3^K zJsYHO{2bmo7pzdr`i1Xmu3Td`#53Gqu6r@c8Y3oeOk!B0E zqk;R6nXEd&GiZ$2%G59neZ}qS7dO?dl?wt0QBwh`_jLQ0wvaCe9at8^PW4Dr-rKzmJ-c79_4MsR5$jH`od>VEY| zPfrhL)THmvHjO4G4c#?!pcV2gl`yheXGn`pDg|;$89InmBwf_w8=M(MyZ-{?RoEwa zMO5P1>7dg@%?Jo9y%UvWfy_Mwuummm@Y6#L0^~rg8lR#Rwx~C10?Xam+I@F#l?_6E zSd)4&V3a^rV!$ZovaV<#?>n@_)EZct)O@%^n#;Dq@z^(3cQQ4xZ4!=qW}t`SXiBL= zzxQ&uC}6$RqpNW%A(+~60m!fQ{-deAodkZ|iT~XX#z0ykT_)C8?t6UMb~=nPhs2nPhcN`Z%!d6$2@Z3ruc%hC!gBfk z1N^g`>DX{I2yK+q`izBr1$x9jTqpdXRNQeSIQ%a^ei3f@svj3H8Cx_;Iqli5V9$g~ z-c~QeEj@JXAI9BGKmYREm)ZAYIP}n@HqBlf1mit^RG5t~CdxJag3@|b8F9qYwzkqR z)X_mqF@+3C#3d4Yg2)=^h1J+5wHP=-O6qmnD?MmnfK~VEgycA%&00p$Rq`Ug`R#~J zQ?DpLu}Z!C;1%SXXEv|}tb z_{hEeS_qKEZZl@1nWQv$UhCG)?PnH&s?uSsu!oSyuDYG&Qb;4 z7mC9g615jB(erOg_>G>&LKPMcuu64M`TX3ZPAkqrO}4gmRW!Y|v=*--k?2kv#Tv{k z4edB@is;>G_?Y&5tRX=RgCYCQ3!Okt^+!&Q`y^kl43hEzMy5*I`A-|gd4sfsLFN%X z1G4(WB7b|YO!2Z6pjbX zvX>g-!e5I5$S-L41hLu$#nt@cXqrHJLK^LAxgui=UV!v|bj`lClQMi3geI3$>E`p5)DxJ^ceiNWT@Lw^Wjk9}h4`#qZ;}rxk zbFz{lkC2@{5ZeYVgpQ!+tJzY2yZ{LJm*Q%g!~_3ziT$3TY(Po{nQLd05iRsz!el>*f+$_!WHT=YZwgj@YsSUoRB13;o(biXOef<{3^KNYY+E6R-GsopO4zDb<#--CrcRF%FhDb@k??4Isnm6_pgB|Sg11C8b&nqIjHFtD^ zvRTzaXTaI#B*x=*tHh*y1VmG}-p3g&@1oKPNZM!}-4Vq1%P|)I4s5b9;_zQvmvMu8%@Dn0P3aR5oPOBSv zgU|$R|Dz>X(S9TA9>?tepr7M||NZHCr2pFo%;;1;5(Dt|fzTe9)x;+&Q0#PeHCgJ~;oUiqOU&>S#iDpm_KT8UH2ia_l#8 zjY5f24>W^ahT0TFM;~3QHv4~3M%bAnz-FlZCoy);7wrAjPUj;jU%a6` zegg+NbX^dait1@L5=f^{kQYMvz6H&o=+(jg0@Rmq6%woSw++c(!xQJTzu}Aa<$pSL zWrDsG1(iB=n(7vWgZ);q@WfK-0k8!~4wGmZK)xv*c702$`Bh*w$+<|di~(D;J&>>P z^N`U1;?Jc3JchO=`gjf2BrgmZCyqW4ZR^y6ICJTe2=#5VW8-L0EwIYEnx8F)DMK#bQgfw^uFto$WO2oUEt3J% zu^dxmIOZAP2%)CUQ+95#;J~S$_%zfS!a>GM!8Z?CHoZ5>5D4hSoS1oPfN+xuirdVq#{+wDfac9)CcvQYev;R zZ3L0Vt}cU>FComj(a^yXY_Y5_2={WKhv0s+TKr>UXPDJ*)CB$PpH{0wO-3n`fh11l~jajW$^=O-> zw&6(V3i}~t;o?#2ZOLizLyIEf?X(e2Bu9Ut8E0xWk;OadV^!ELo8qg%nTRsud<2F_ z^PO1dALB?vc=I3&^)P)o@cD1>+xCoou)A{ua0=NxZJ^fCn@nX3#63r$RFSql<5#E> zjjtwblv;yywC>s&)v~gW@cE7r2R9it@JQeK^~`~)wd$gxJB35)hV-4rBevt_`eH}I zpgh<079q_1IWr--w4ywbyHME)tW$)-)GQLPzC z8xr$x=LN|)%)2;%0Lr~45B+#hbn}57u4A<{#8WazI-T^@Au6_zO)?U-_f;p?^O%QP zjuk$SMVFq7Ckm!q;9f~ZxSvB%**I)m6!9DW>$J|W$e??+mwbLM4wNlTyfUoKNw}c$ zfuO49MQR$(m_b^;K>00P5f!{~@`nw#km7lS6OJT3?_0WFHfo8%N3Rj4CXjeQ7#{@-8)kc9-@ZV%AYA`WPu?(aJJc3kQ>ApuAkV8kJX>| z1r@n=VmocB0(Ie>@{y1Pq?4}{5-L!uVDAG~OYPl_N2;7D2s3ue6Y?%KnKZhjT#(F| zMWv86j&*Nf1K(Y{@!VFnk4EnSb!usPH4T%s7RKnp`%3q@*Z=&J8q3{!esJ6_C4W#u}l zb$%oL7A8nV#Tffg_RKuJc5X$tDFVhQZ^QeP_9)PA9*&QxPH}P^?u2vhbN9BRni|2V)ss^4*N7Ide}1wO(ZH(>A)p- zU*{1X3(^@oqs8vmL+WZ;NlFF?`j?fGl%p?-EE!-})=T3=dI_Ava=x~)cLgIY{1mZn z8;;=^R89{6pkuGwEFE5GhDjqKz0=ss1!V&nf{B#!4qB#n;kVS5zQSkW^pK)t71qo0 zb12xkc#%luT6CRw{0|Ecs_+DNqVh4YCZvh1t?enAoS1W?w!(ZJtedV-Ptv%4qIrUR_zmx?+-uL zyEM};nXkw|(D^MbzfUa6gyW45nWe=T|tC%!O z^J{9fo&4dxUUb`{q_c5Q+Jc;Giyk^n>eRzHhdahDWFQ9Z^f4%r{+o26b{`s@8#ll~ zbsN-Q@y$&o5wvpQPGJ1)e??>~xgj6+hJsej0D;#a9`#1W3bwQatm=sv=~~xlXAGUj zy4Bb@apPk_`0KeruP3KPm&`20J8*l9Hf}nFQ_8_W{<&1e&>@(dw7-89(zda&3n-OR z3?DM_?jg^K(lB5!xlwShMJ3H8A^uW6yuwcM5?0bolsJ5z5-GKj7Twe$QO6h97jy|* zTun(96PIRAhZ=#RX<+lG-@>QK?m5rn%2^0+yzlC9#dJc7*;fMYU!iT4N_zhDN{Rhw z%kWyH1M9Dm{hb#+Sbrq246ILPo>KCk^N~)t6q|#4#{;j62LCaducUv*^Z^R1r>9+) zo~%vZD4_bCDi1@=&dtpYf_UrU`nl!ECj7f)S?*R0+Wgp3lsAA$beyI2A0s*?|4yn# z;`d+|xT9hpND$|y0!3@=?n@g-&A1le%q{D3Qz`9O^Ik&9UbpHo57ITN#Kpq zG;_M6G~;aUzcT3mLd7#G0bCmagmq#n)XmF)?=rMPWS5?qpjz^A1rak4cej9x6P6to z98w{|+8`2Q!od+p7`Zj(Gwl0x$WJXS>|nX9Dzvs|jrwz$G(Lu-d5K{%4w4wt*~9*RVcjL1!V?3}qDn{|+EXgmk6yF6UU;>`{Q0O# zfM7)O1)f^u`qf(Y{0T$fn?`0x`qNcv^4#yngQ>v+`z{eC0Q&~QY01jxm2dUQE7igb z;0)bB63bTFUU83;AN~Y;{}5JHyay64*(Y8{C4i{Wn!3TeIDS)7%@8xx{d11e+t$YY zX>PAIF<*Nt%tH^%4}&d!?`qR}-AVv!8Uhnggq=(4&mfd5dEoX}Hmxe|nF{J3rQTCh zf8Xcw^L`hxF#YkhO3+VE;`>xoZbq-;gqV$)%6T{y{XiqKp9dMjZ4>Ca3X0!tidf9Jw5 z&oE!il(R#a262H^vNC&53LAh z@zm5hA5Aqi=i2e)RXNU?uLp@)+LdoAg-<%FH3rF#9C;NGd?Nq@<+=K&&+yB|M#A`5 zAMH`rn{-D_-;Z~_Z;pMKJ3`FA@}J%85!D7@CvT8XpxRljLFp<{S92e!oqgs!92_Bx z+~%f_T)u`hTFOTY4h{Mf#^Sgt)GuQ$H! zdk-N{Hc96{ZqhBK860)p;}Apr>^Du#P$c_e?Lm%cYTUVORjjA5HR1bYQzOVt9#HH@ z_gujO%qvL{yj=L^fWA{mPAz=YjN*7+&HJa3nYZv1=vF(Qn|XHtZuv9gVXzMd8ry>b zr1x9ikQWpfALU015x@3waBz(xZ6*j{Ur@wKrS{AGZ^~-ugD2M$h4>iBCj!uSPzl>} z@Nwc!JjA8`tfi@HP~((fu+P4FZAIR#%fn#zDnXj(6f}ds$jHbZHz190abtYE{i$4) z$n%JjxeSld8d(EH9OZK0fzm6x^=6E`xoy2)CUB+a+1iw!n2nP2*m!Zf@9jT_*asg6 zdvCPK&EYXSq}!PG4E2%DA@x8+K!D%MV2LIP<-gY8!r% zQ<%F8ZXE)%dkSJkG%^2;*SdbAjf1WR7DS}k=*w6wH1>K-AT>T_x zZKB5838cA`5;67M=^d6}eIk~gVMO4x6Bm{H3{J$J-WE3mE?SN^&d-of74?(MJ_<@a z5cL$)&9*hzea5u9L>?}#L0oCtSnV+p9FI0Bac3L51FmdycRsA71aq!1N*|>Vh^*O& z<#0kOZtT->DSKDr51zk((z`Uf{~>~F0z_%>+ACp`W!U?E{Qt!v~s&F5Zr^w*q8$i0IJ^)HAL?Xfl51& zYRS~k*<%HM^k|Zi*A(X(Xpvy96LH{hdWTLSTi&E+Nc!pC@ZP|`w098OZWSXmA|i>6 zv2Llnp_%TOn*2)NUi=pgk^~#jNTn0kQioUo5SMUjL4NqI^8MDm=n5Me(4mX#D$|`l zV;Y+oss=w(4|*?+ob&iVn`yS?Zu`j(UboYXiTGTJaHH(~9GWGr%dVN1ook9YhAq|~ zrDQhaWSywi;|uY>X4_7QB=awy7fhPrP6>R-G#?o3TiFf*%vsL)2Ai>BGcJWzZJrjl zU9z~Yf0WNSij?hCpQ4vH5#A<*(ZLJT5^v>GC7XmYbc0o!c%Z&Y3Qk-4~K@&>LZ@KdLb5@;Tr?nUYA66%(JVUJOKdK{3bu;L31-;Cm1@{sYF`ay4F#Q>e7zTPA$C$^7Vq!oN zN@af0*!lxo`8PfN>i#QyZO3{g(QqTaTCi9X;`?;l0VWaQ!?EmID~ylWYr(VD@)}>A z7U*Kt_!jM!PS`QIeNy?>B^b{WxA*A)EVh%9&cAH1@)c)-8?fBZUU3z<&_X7{!j>^*u#iu&_%gF zB-79CI$ZNz;*kUBUMY{XZC>xZHY*Xt)uR_Z9fp(z*+O?nhf+n;5F_SHbLTzE9|onPVO?cDDuyz+IrU}l3a#N6x~M6$$CmF<7_ggx>dyy^vR$9zZ8k$afa!3QehuQ%UQ-Fl28+b+kk zF6H+n{AQ*GHZ9U34pV9EJK3Jy0XckVX1{r0hoPoAOya%vuaL<{);!=Azp zPNS%K?K+1Te0)k=$kh;)NH-O-mzo-M=OzTm?88}t<_h2`b-Xuy17?Y{`FpZb0dtrb z3C@{$E64KJx058p#Q5I`rLmf`#CJ9v)%{onLMebCL!m_0b(6CI7sq8*6ps3vWR!G< z&$%q>NtS7H?yCb?#iOOmY{2vX{S?bzwkhU{@*~EDC3g$LAp5(}g-xc9RU9 z#QvKH30P%#<=W-}&=CtCl~(3D=qkD+nC(QB*)9`}j6l(+2PzUuW3t%IFYt5)PJLK* zb8oUJv0b}spH~9*TzCnvSjDbQ4)xuV%%^Qc>DCtCj=K6E{K6vsd4EwAJ!&OPn?HQM zuP}bdZ=RKlN>l67SQ5w&o>~qQjS;H)~-C@G) zk0cQvFa-I};H~lt$GX>#$6CC2C>uq})KW0;eSuLTkDYii9@9>0sSq7|=2D{%=J|nB zu!Z}e*FD2y3>lNzk}ZS>zV{OYnT9ufGlTFcwdvng4st}XSapx~lbT|)5LTNv&1F|q z$@S0Y4xSS;NX@bgCXyo`yo=+qQh#!wVy9+LhK{l0sR{RU&ttN{1WUdFLxIP+3?7G- zTjwe+A-5whm9{?Bt?Tz*RX~*1w(r^vJ|qW+v34D}kIf2LtR9SIgS`RY!djhK#rT|~ z9lf*V@HezK%49&onN`yifUU0&_4(g-u_<5L8&fWGPGH=OlmA7c+~{kv*RX4k^QL_D z;;ZMO-5{sR2na+p0dX~bD=VitvF2U1>v`+8?&DKc;97jyA1!A-|fo)m&Nay33D zt1PSM48lZBjgk+%e;r#}@)+v;J()wvl0k{vANHnw!2J!j17@3kV0oL}Um>)+{7{N{ zV&Pcn*U1!b(_}-f1cyyy&HKd)rLz0ppN}!rFbtLjXv=woZ0=>oW|koZ6=m1yuZOgg zlsNVB%v9~qg5}bB#1=L?S6Eyrn!gzI}j+ z?yjhr8X44Dd~G{7H>95TSV7;$VC&Yo;MPY|EEPqs_X8dU3RCOqjZ4O)=AcYERA_DJ zK6sC28~)o-Jip*&i42&t&b6Kf{bRb~QiI`gzG>Z}gu>Z`;JjDyUM5+w29dnHOB7iC z;85o0*=xNL`npAyKdyj5i7QbD8J$zNnCSlUOl#9pg$hOWQUy_TYB8u-o0z?b@OGQ- z^md}-#q-7BoAzS+wQBMJ9D2cALMrhv+xVFt$C+G@u>^P6?f!Id zrwKqtMFYjh6-8(Ad_YlRazJ-S4;UjhGc1c0)<3|7BD66&62xW5UHk8lc;+o$R0%C7 z0M>*o?*_x(fVj|1X}{}WMMR6RNhB3)DpwZ|ec~l|zj_kf+ON0AY%2nWJ)VoR4UT@2 zQ;zR*VKK_coRiMwgVn^o-Sfu_0H%i78FKQ#fC_!pHe;}6G7D#J^h9Wh>; z@>SzSo5_B{>6-KMQSPVt7~#CUMYCA}Q|Q*IH={NiCehqhIaPzo(tgu;QGsM6s_ zz42n&ZdrTFhXGKAiAjX5s-6z&%ps8iddw2lay~WC;)@n zyv5RE4=_&*`KFB%<|@Jl<3|j{o&4WU1&cjdzxe$ESk`Y}srR#((65>qD++ zqy=dBi8^{a;1ftKsn^l>y{EP3Tbgl^U#c*Ml@78r&0Wkmw`Oht5~NFVwl(0F3G3hI zOQ_`w+5I|6NUg!Z~RRBc9%m4zW)=z^5nzG*0Q3W>!-l8LIGF4F%!B@52WSWnYsK&l@HIbt8j5}1j(zM;E42cVKW2g zJOElMIMbzTd+?mqJZP=QQl?u4^`%0G*3z9=FS-SYgh`Sv3dqe|2q3K9;c-%eBJu*N zRiYu3YTSH)2oX~X1L?bAg6X^rsBYsB70^NRusRjJC|Mzi*1&4Jv_510Sz+<%;R{~a zF4^{91A|Eghcm)p$4n3Bn*J%VLXpIjyTjpjhZx$f$aj9d#N%fxbU|Q{m#D3}8CrL0 z*EOioO;&o_fu8=)@@ehVFpm!-bF0)KUh8!NMTlw@^vdM-YF-au5OI>x>Du|AJ<3_w zWkK>+Z33t4uFwm3?xOalmiYt;wMA1?gY8clKM{+gnAaf}YcmUtLxl&85*~CDzQw*< zI*L^EyePZ%+by<$(X)H-yP}b#Hs!J1KJ~X%hla9&#h2-yNc0!qYt+n((Z z6BrW_u>{isQ$QQYZ)#0~XK!dCdVa#U!gy%*K*yE@coN#c2s62_pzxRG3HMhcIEfp7 z8-iXc(p75m0ff5Rna$&%9RGw~aLnu70dhC2q5Fe~0*o{KQ{E|BMg_!-`#jMBH7VGD z*Hyw|9pmTu{b9Wp`#tix)YPK;DCO1qs-jW**(xB^J z4pHxA@q@te{Z0&+Ejsh-iobfL$Obw=7l!`=Lc4W|NkQccP(mM$QrR2m1^xSF&F#>k z%>Tdp@(ND@gr+D|50EJU)~eIpSa!`o^I-a8I=Tv?AwlGc#lU?D84@vR?T#VFs)f@R zia&Nw)a?Rw{O09a_P3_JoKw)q^a19usp-7*#CV3kW3S0oKSQWa6bAj!567HDJL&B| z4rr=o_!`!6DR*w6SAp590I!%h$^x4`0CAoPmo?O007>v^vB_~jO_nbX90|kky}-@i zp3iMneXb+si7`)kSU0KJY(Fw^ytK-7G~p;<*a~V%UDjH?Vrf3I*u7~{E{?`#M#4oVXnfX16=QZ~aX_=c_Z=C^e;wRoBS?NGS4N+jg_?vwcS zWk)*kxb+`88ll<%HtVH4?{p57u@2bG_R-GcRL!?|TCgm0?1jolM;B8JI3=R5*zyDr z`PikvL5bIZxG%&xdzTECg|#+zm29<2~G_F%fvjZk9_4p0ke zTdrr->)_8^j*|$|aXS20&Y+j+Nv{N0d5Jg4F0}I%hgBvbYu+hAIra4GH46%F-112w z7;JBD25W;F8g>VOX&2?1j$V<=-Ya@$_gQ%NH>Vyf*Z2Ml`I|Ct+B|;I^9)2gr#(Jz zK)hIJ8GO0o)RQA`JG2b_j&-R8PqDtdZpywKf2otGeydmfB2&HjB7o%6nxN0Zw{6eD z;*9$=@C%+gZdO4>!9`^7?@63m*EO`>x8*>N7u1<8a{WaLl z|5vXfGi0kzF7t80`f3j5%K~{ObrU51|O1e)dDaI3e`~;C!HJY}=E4*Mk^SPjPM+7~4&vyP-SBgq}3~st_x?XE_LV z{xqGX+jgG$)*A7;XcW+&5QXmHwSMaXwKaR#uMw03QClMCq^cM>3ItG_iO`8>7-VUk zZooGB=bwb8hbvMa|9(0-O==~z=p>2tOVDL|0IjJ688#rAn%IovL^AKb2n`wgBToS3Hdbnmpwx6a}W<*)7T4Hih|mSu%GRJkBQ&My4$^ zykSDnYOAi5jK7Qnh4AV;BwrxfLt5V86LH4A*X+El=H3?E;+UlSV|kIaH`039LowXU z`=1H5!TF_t_^ir3yqz&wjs`Nt@YiApm?bIuf~{SIpL5-^x$=zWW_FDF#T%QH5V(Ve0EXIiNn4K=b;89J%8m!Likw1Fo^R7e~4FMk}X6u2R) zIUnevoAg55BVg8>`{SAGbh|@u_q@*iYKm_V!}RwPYSYeMM*C>`!(|KJbZT2}*HF9I@O6A!QN`Ue`0zG~ z=7t2(Y&2pMo9MB5B?yQb@Ac2`oDavW2X5-tFVBF669=TL^5^H{uo85|cYS2ZypCA> z46XmU*P?&`fV&0$jKC>07st}@Zdk)Me9klbL)Mup>TWU*<@{6hgH|}S%^Rxy+WNGD zr?wTbk-AlV8D#G`xt%@SnATJuz8U>< zfKOj8zHwio0CeQ387*SsI88oqEJ2L%*rS->X1&HR_!TkxDj{oDu=f{ zAa%!`dziw3w+)Pw_%sFqf{On4uc&s|`H$2~5T~xO)83ZRGU1YYWtjPQBBv~AJ zOx4kK8F)WN20CHI>;UXs`}Tlo^M>|h2bP!>V*`L&l*`=gA-I!<{EJe5;TRAbI`|QJ z))@Adncnx;w_)Qw8HtJ=$ac+CrW|r2n?$UDa1O>q#R*=Y_ zdc;y4FA%Xa?-S(A3p(;G9WJyLa(0(Yz~SjsfpUg{W@Y z>7XS8fYydGGqyE2f&wlAg4mzwqHR&LU|R3v_K3F&V9^LN{b~44VmwY?X+t&k_I#c~ z|LZk4v6ZjcfoG2`)P?jTTo2Oewzgj(gwkIwB(Q1 zD!rp>cOxppo9NC_f+;oZ?5_H0vXGQY&ABxupOh6w~ULh*S!F zFYL0Sw=>%h2n;G(dj_^-g_|13B3ManFyRfmqw7b3#dogsE)T+ zjM>awOj#T+w8*+-@#V0Siq{$1t$W_T_5?nzjT7dfZO5+6g~syOfF$=w{o1=9Al-qI<0xQ2KpnB)>0F;YJ? z&uyYE?5)L-sm30#_7ny{H94?wv8B9YGsA!<5e7TQ z3M%>8-RTYbQ@o(u{h$6i=LDVws>v3WBbv1KL-j&U3mY(0wcps5|KIKiAxEgX`5tsc zeHlU_x!>~uV!Xp*#EAuOXo9^}wCG!6z4y-2oh#E-uFZ5_v)>(n(jH6WE8wTWnfVi} z4Ic`YpHK4wT3tW@vKJ8ZV`H6iv%rsGn#1Y4U?Ikyf)*|RX*vK08^ePAKioU9?f$4_^}U)&h0&IMje!oKnq6m+FHT?o&TG7~QZ3IS|##Ijx0d%xh87a>|P z_ayj@hS$BI3(@-`faQwe~P+vfRwN^M}Mw zL$0ms{8ec4DD^jgjzT>@jk)e_I9qI9Nc&#D_fh`Yt=_#{9#O?+b;ulI$vHT(LoLDX zpoe>f1p|tcj+nbB`T47v-)k%JIs5B^n!~IO|HX)-KpJ;iZL;kl;R2Rds~md7qj3AK z>aisrgE)elX2aVWCM~@|2`=d-8fwG=DeY44rvZI_6!;i{f#C(F&`I1@!m;W%RSeEF zFA(4At^Z_Qc!2;5%>v^QM(bu%R%EmHe4iHj!944fWTnN5j&tCTrGct4#n1Uhj$6g& zzN-+DsjZIyt$=Bc+Ah)}Bb|e!7)yDWXI#wb)NHg5q=5fuq-?g)F&yB8`MJ$DVI2*~ zJ9x{xnh$t8?hBjCCur}F3dj`%*J$YOuQ=WCe~IydU|hGT7QRsSCh}+9XpFXM1L|>! z$Bd`PkB7C~tM3(WZ&h)^nm&U-u{s}S_L<(53SV@00yw6@EE-VzAQ$G}F@d8k51Csnv)RC$MYv7l892dVGd zkF%+o-o@z3bkR?R)dhFt`yl$@ZKC)!wjTg=fRvA|hg*CF_6Na{DA!|K6Ua(V!VJZ{ zmWF(r`{S%YdQDhi3Pk3+njW~sFT0QhoIU?*U*8e`3TTj#3pO+lyk4+rqyJkc1V0Sk zFgcud^N{225N1wa_BqqE}@fbnRIk zAO+ibmt{V_e6qs}6kac!`Zk)r#A}uSX%U55?eBePRrzJttBkJAE)PJ%!o*JV@xbtQ ziD<7~eju22SD?TL%BkOvnA@T10{qpISD+OIG$K%;%Pq+?LrrWd7G!Ps-ro26)aG#D zOx9|}aJ@9PS@5~_%*pk45oD|4bbVDkGjGYQ$-1};EunYNotJ`*_M_l2QIf|gMqhlfFx_LGFd1E zLltMkrU@^IN|2{&?kD^V??UrUp}8a4p-8aN0K8XYmM_EvFfL6fRO_SGCT|>gp5!juQib+8lyqX41-~; zW9GeXr}KW!`+3iQ?;mIWozL9&HP`z6eV6-Ie3I;VK>CK>(=b~7-PF-oyCfjiNOK5| z>p(VzDejZ>|1*1_`=8r)VPKV@ha4kaxE4I4WHy|@3CxZBW0VZ`OjeEe-dm|jeRS_W zKszTuGE$t`6H>*cYNtS0VRO^u)?4P-4H>h}qWrJdkD?pHj)BXo8-e3K71CcyE=c^OIuT4%no^_*lcwSHA!6IZJv@g<8#+(X{zUDJ#e!Z-~T!qk2Ah8 zaKPgqeeXLHmk>zy?C#vd+m?#J>=eg23p@m#fB8tV(Fr3nLW8j}J~W&-cP0{zE{a`2 z|FyXqh#*T(T+OHsQx}|6%Gm{GPtV|r&`PPMf?#Eye8D&H=i3I3uoajZY z*3ce4sga-)HJ**|8ua<#jb4}1v%kBMMyCa2-JXQx7(|JM0TR7VmVvIjwR8jq<3-Y_Y>u@{H~aln51DOTW`y)fV1$1&%t1hJ{->gGp` zZ+Z_eRIGiw2c2&%kS*wB_2fJC-L~-;Z+1A_72tUdoM5y1+@P$SZ8+)J*c~C3{lKx z_asb9bPLpv1CHjrBun;(2LwET&0aTdq|DgLTkMtPJc{vqJe$kJ+YV&_-!5SIg$}`b zN;1pDxU!(;mcOR4u>MGWvL?d|Xvv>tTJ;|xP4^t2D=FB{LW{nRi`QDrD7!rxu}Un@ z{-WzM&tU(N8S!Vk1#h9`oqA35ZqIeT7{QE{O3p39hrN~omoTn8e_VMKArvgsV&1Ri2a+*df zMpzS7QmmuuRULfB#(=o;0QxTkI|x*(0fgZFFxO)}0hq(6F4L!wo5#RJ7gG}bo<4_Y z*HX-}iw{JY2icX`H)tefJ#U0uHm$}H8-6-BA3=7{w)qyP!&9*nOQOZ)({I2XK^c!T z7ePZkpwG-521^bsJNCtS?F?iIxoE%xuvK}M1iPrlG`JgZ?s;Dz@;N9Vtn14Xlz>@K zM0`_7DX2oX_;wl9?r~lF1{s2F-tM-;-9XgA;!TQd6mzoS&>{t=wAdy>5@~cs37=#p zuneg6BWD`S$&X2CU22*QYmUJvdtS#{me*~`w8vo2EZoXQmKf)G*F=Ehzg|q!_$YYa z2GwB99YG5M;#oj_un-I-=t8PX@M4X{A*Mg3^|-7re?PrdpTHneM8Mb+XWo-&RrXI# zak=fofOs4Z^OX;SuEJr16rkv$cq+wn4XLA(7`FZ_o~K-bA${#u(NV< zzfSnzqMPtz*N^&}G=Vj3ePY~9$b=8}O1ujrFm4-A6{vj}<#?8>wCx5#?36mI^ue}- zjf!K!U5YZX##ZnEX+d5JOjZz@auS;ptOtNlVKv4Y8;wOS#^(;nlxKzHvD%d>*;~s~ zC82yh47ev>0Y=sDo>~e2SE$`WuMHak{;l-2^}D+wRfA@WBlP! z3kT>R>vtPnQgr2G*{`PT4}FeRPuU1mJRfjJ1f)X4g4P>WLVGSbpAp#YuJvx84VECV#V;VeVh-cIbrc}PPlp!mQ>zHC#RO93NVL?Nly}!TfyjD#2h~Q!`((# zz}a1p?ez5G%WqFeNJe8X`wFO2sP<{h`YWJD3tm_K{{ZqlMHbi-g$^6&TsH^1@_+Qx zTL#H@pqIAoe40zwdYSwHMA`p4nC{k9Ao;NT+24^`gbb(({{_=ScEB`a`}uaiPbmuZ zTIu^i0T#WxVDE_^LJ)r&0H!f@98APP^b2I`15@d^iIVu0`&1{8tx*2OJ>1YqYbqB8 zO7O!T8Q4zbPGCMuVpi9s3iBmrsacO4@&#woeJOzMdLP0>UNzGkFi)VbcSine>;e>N zDrYgPd@zJPH~{1%J-coO3ZSY1ce1bki zlTE`-4PPefl9VjyBt^dCXzd##A1ghp0c+-RO5q(KRFy#Mm!W_}6B9@z>kRO#EDm)< z$x96^ABRh}Yl;VGo9F5rvg-6C-GRI)5Ib-`3AEAi08$oVp31Ct$_yE@B1D(H5W$cD z*!o_%&SkCA7U+%Q-U+7ziNNcCzkjrEo)Q=L&-m_?YYd1LyaK%IYyBD6?7gE1OYPc` z*%^Jz{!UBRTfn_3`(uEci-YK%98Q2=Xz5!*xWKm$kPMI!ex-AQ30&}rGYx(=Yqjk0 zDN)~CuQoy8cTR-8j&?q4jc(BRxYh5{9WC}V$C=CN_nmR zP&x+f&y#oQjh5E z3@eWIa{0T}wplE?wh1Ih=DBMs#a87>{}%3wF6y5zo~RLEg0K0}w%}gKy+37PrFM-k z6zkqN1>Wy7HZ~Hlx}Z$XTB!vxiSZD(lJg?z&EUm)&%orOm|IY}4Bx2bNr;Lzl|+}h zeV`qSABuq z2X8oNO20*5tK`NQEq?9&W)QTsMV={MYx#Ul@{5iDsxn!)#sG}0(*W_~Lhf4+>LWG# zjTI2Ypf>a8B%jWbO05{`z6x+Y2%kA?Y5Zq z=q+AtkU*}lxvxZ9+T4{HuCnP8bRhrEYB;>8u-=H& z6!VeG;x!GOh2^t4vhyxQwT-4+mRe4W?5EK<9r#JNU}T!1vVgro%3vfib&LPZA`fB! zd#!{6cbr3WE!vV9ADS12e0;txJ3AZ250;HgS%f-vpCyW=pKB!U%Tz7$SN%EgwicgE zq*yH$wYCHdq|;0r>R&4IbRtsjUbI*Y@tCfSAZh+~)TP4)W$|g!M#QMReWKdJYVnl8 zl$q`idB)P>Ql;FmXJO)^Z&Wwh&bl9CddH)KFur*IMzF;LcMa1W+?+^W{?88Ot!A~< ztUQ)-_%m<_!5(s1Ig}k5zLkHedY92htCN?9LZ&CI3U65WFlozEof}R})3%A}9Qf`G*5?*D-lJIiBnC>(1wI!q z+J+&J%BHg#S))xDs^NE+^9WcCuyU z;Owx};=SfW%$L}oW==zR?kToUWYKxj8Ci?Ois>Hx{wpP@H+ju-KJjOBdz)e=W9{Nt~{!x zsx}-vm9Sx?n|ao8ZQfwWx-gDp83wpGge&V?aS7jd7DShwa70(^~L zXH0+UxB49OPTtiI7b$8LtQ)~A=PX@@gC@SBNWO)=Nx;6|{d5IHYqxq*E*1J9O!@r1 zxMrMb`FbUYMx~Iib?kf zy&;L9u^|S6c(QVogXey~&gzG`&h%kIx=YJOGKMprc#fs(H6N%BoOa~+jQ5x2uC(q6 zwkKFOamp8L@;FP@L{^7Oe6Szmf`vS$#GLse=Q#%eEZDBTKli*}cIsU1CKVbwm#H#5 zbf%-eVQ`g}!&H^%n(UL;riba_w3`<>uP&W27_k;gh<@fqJsw`=1|Kz!DM#I&JAj$( zUR}Ox;su|%9-)^lfnG_aguL0I5x4H#uF~L=V^!PBxoWyI$#V=mmV=hwb-kK0IsPPXkE{dP&Je}9koXmygdvnF}OZX8?Wn!1BjGNxW z4XuG1pOyDJbLm0tKBS^m>J8)eDyg=av^lj+72;01mmp~5|L-r;J5iVMu;cbvzwWd1 zJ*>!-b(EMk3Owe5Klk)vI&&`iPJDmcY|u(Hb(OuVML%6h>hqt|CK;O^a+G~An5pa< zyC8ctUCcT^B{khWy52zw8SOD%b4E~bC3we;m-{r{xei?ri}X=LGr_EPk1y;CWUqEU zdST!#Rn?37@Z@+P@fp$L55Gi#Ac}a;PSpvj(bQ^{63clQ?6d7V&Kr{7@^4wM!<9GW ziC%tV=$6NqLdYY}=Gq?QxcZ*K-_8}ui$9VVt#I_+Kr2Qm3qNqqkO_8N9(jC?{hT18 zK~-TR%HMs7uQX^*FK*X9VSW#0n<4DL6fR;t4%L+HkDG+1EPrq4B*9GOfq zm~)etK2;%}`nc|Vh={oKA;hBNJ)%MsPdWN-_m&3nxEi|P)+=V%1&M3fFb?)=oS1dH zlgX*K6X`*czENF`k6Wr5i%U~J)-`a6;3<2f9N?>A;=rl~#?H}}t)V1TToP#`X1%@H z92qoOmB-og`e_Y#knF{|KUajoc#(+C(}XnsUWLcZ$6%rNF08Rw`!d-unONd1Z#Aru z8sASvxnOVv$`Fre)(gaCNd;1>H2*X__z^u zcFcvmtWGsLnVK9a+O7772{KUU_k*y8)DtP;2JF?8qo%tq_6mL?lVL^TC^&w_mz1t8 zb(55Y%mQ!?`yz&aYQSwn+N$OlZRLK)wM@h}BmHLa&cSr!s{0#P;^@O*j0jdC@Z$PyD;3X; zu*-N^7|hw(kzE5?U?tm0@RAYLxrO0eU*ZsU?!T< zw&l%0@!Fp=ERbOs&8=3Q*jq`C=k{(=DPEGwD|O4dlJLHveJ+WvA6B`fkatMVYiM`7 zEs+AxLTWaKdCjHv#D34to-zmJKVK5Tbku*U<2%g=2xWEbS?oJWUnNdOjy~Soq2&zv;Z7|KuVvIwIn}F3YD?E^Ucp z2YkBz-tPAk<|@suZg);XE)0h3obZ8j>8l5woj`BC*$O8TxzLOjzv8WsJX+sH6GACZ zGco_--ayp)q5i&cm>lW9kzp|jJ+I{y9$o{c&2!a6z)4M1(onRyCEhib69!Wl={*|C z{a|i>a*7ZD+4u$1PDiXVN+v%~7-$S}< z92_}qq9>2N_NG*jle%tCGrCVNXr3c206RgZ2WO{fZOG17XxwBUbR}DE(!j3Ech&P? zLx0byr|P4Z`0%UO@$Lb2{mfGJpELO{)caZc^wsI!i<)6CwAiq8wdKK&e^td>%^=}E zJ@9Q_^t?~wY*E5E36O8VjlAd z(`q`LJL@MT5jWl}{$#3enP^7q7)u#MXv%eCepHY8&pgmP-x1x74 zWqjg>$EgPvONhuM#jiSc)w1;BOw~Wm;B#?m=p?qg*mFES?IjjrfwuOAuc6^9I#>Z) z!ZxHTA5pfC76JB5*l4J63_Hfpmh64T^159!B9CJ+6{$`djxssa^3lYekBf_~kiz1u zDZ8pW;6ux4$30n{!mLoWvV^@Q>`dl1J&Y6hw;-Dv9BE){!1-EBzTcTFTKXi3e3={h zO=%&o!CZpVJ`Ig=LG5yH3{w^$s{lUWZ87OPy@i-9i|5-Gd}2AwY=jbDUuC)N%j%3E z;?3%%T@_K<1O9WDV6bq77tAHCllM2fQ_eobD%|$5YB+|VzDlekr1f0$C1*JI~9bk4O zOF~n#@hRW7&J%+a=F2GY1hJCos@o|DG{vHAtDu=?lF>t4cxId21GmT?R8Z#%5muUoS-dUkvNRnse#oElg5A1(umUDg8>+nZ7+!BMh$yUT4}Qik2UX z4Enhh-Kn6(3jqJ&e3M;+n7prHWR+?W#s3ZldtM9Y*k+l|A!hQF`UQ_QRe1fnP>xb? z4F$II^5!`Ao>*}0|EgC*z6L;(X>FM?_SwE?3!^ zhSKuM^dc}oR2mB`^ozn_RtoonfpgaR+N(jcl$lGl>$qe1s?#eH>KoH0nGjWhmH%t2 z{CMMJgn5I}n}!9X?~)sCZl#(Leb94zvhr9@{CAzLj&DDht1>+kuCe4o~YNHessV;`v}O7x%Ypz$a14BCT2U6vuT6p&J2)*^0zd ztuMgcBfu!t#WLdQ`lzOg(KFM#7SEXj*KNJlfGI(sahdNu#7{~rdKZTud0-DO4sEz` zaY1)Tyn}T#K+T95?z{=bT?9Mp9xwG?%l|*-AKU5wWB$2Z>(mQ>^=^s(A`uW4ZdVll z?CKFOa^o=auZ$vAPW4vb?wN-pw~6WaT^P+uKvNgtA1dwzT%6p zEoY}1&F)%6(Vop4x9`v-;X$-GxS@dZd9XwWn{P-4Ll&iAHC8_9DSX9m`0(ruJ6p0e zg~eFH?l@Pei>pX<=kqp9_}YkZQtoDziWGj5G6l$3WtF(_H`QX|>$G=V;`Az^i&Bmi~pM{Cab=0z5dE^1=`sFb5j6Gjoa5an#*6`}! zZde|RB?CZJhrQqAS8{W7%A3fk3tbOZ^uUEPgl1)R|FcgcAZMAXY@sycu&Eow_7&Q| zsI($|=oTf%7t%tPCKTgc0aq2{*1p=@E3qXEJ&&x~d;u>J%ohHfnC0|O3NkOlLWG1* zS3YvlPox-4GqwzwfB(U+y|A2^uJlqbnDm4^@g%G2Xix?vZOit|Ms!Ta1tE2MZ9O^) zzwz_2Qpi^;C(@n*BIN;?YB4;Y!z*- z^VX;zUP@HuVV~jQBG1!zyT%q%@4Y;Zeyy?G`g0>c z3ieozH?NhqDnb{LSmrZ9fisN+{7J4aopMJHaH+BtT2UV{ix;bhm^|Db79!KJ0!;!m zdi2(gIepN$o>8Jv{kObZT6SV%yg zmn<2a9XOPvKdklfAB2Szp2ZJ*U#+Tj4NK(?0#7&xsIHOXW9Y@Q(75HF4S9D-j;6~F zRs0va?l{;`*HUOsGCoA6ekWFAKtaPCi@1G@)3o8UNZiXdOn2F^+=-|}y=jx@WRoZ= z=0q00;PW8}9mjjeShm*6uTS>KcQ=!0UodGgKIRQGq(UA!b^nzQ`wHS+ANx#%CBRGE zhrLQ;>rn{NX z%2}PWkT1mylbS8%)wvVsG^1C=hPfJ1^1{v;<@;dP%N(>5p&`qH0EtX`PDMau(#_K= zW&R16RjNx!-3t3R=_&WAgwLLTCk!`7n_Be_DB@8Qg1nVJ;y|PJMgBDeO733j)D$`^{^Cp36sERv$jmiEZ+b$A`P2)X8mE zG!L$do8wwB^{fhRPr-X0q11tqoWdxrB7Kb1(TkbLwblz+$l4$k$y&uydF@v#$Xcsw2kGvpJU>k%o((0t;~ zYPZ~pIHb01`0(#9Pn75=(w2W5D%?t;4PqGNKafUd6)6XT@Wq7J0J@JGyWsYf5Dq1; zH4Jx$Z?I(qis5l>7$8^(;a9+;wrEAUz3=-0J6PWcxmUzs<?jyICnk^vG_+RikuK!;u5tH+08UUchC;PU2y1_2^-!jqR=j!|0widI1 z-)S)Gb{v>Yy1!M?E| zMS{yqCOvt%WzeWI`pCM$&IcfnA5}4XDarY1OI*0C7!hVi7jNFOKmc%l4qW@=rW;2? z554N>kor^dTYDSNf!H?al7AD1Ai}CzdXBdNj`t$n_{pA2hW7o~b1;4&-7uNx`F{5W z{`lHPs*95^7gx&%50@HwzK#GpAScPtpE>zOsjnh0HY0sz@qC}RfUO+|V$yLitoBSM z`dG^8b)cg_3?Z+A69ys?$1F?q@hOZ7%izLAdizC2k<`zrkY=&I6e=Z&k0=jaB=W&m zo)Rj#nT4dn<94XlIiCb3^|%&p*fPWT;YT zF)$!?AQ#DEp059!Tx9YmnE?9h(~LxSq6>cYxAXG5eLv`?4eRhUgtGKf*Pg$lA4l&3 z#lwNd1Z1hl^wVFNycvMYtAOx#n9Fm1gCwE$cf(*4BVDw5OTS2ASU znx}TSIg{tQB^ZJ`GH!M}4+!cg7xG8fXfFo|H6{N!)|j&f=oy}iXw>U~@JIHKbwMP2 z;a!r#;b+hB1#nfl)}%{oouK*j{D5t7d_*3e!oG^k3Z?_bD12N7^M;KB$${~2l27N# z1ShAKgk3T{gOKtPY5^C4Bh3vO9z;goJ;|Oo@ z%kGdKoR=;nfi2VgY<+|7J3mx{8ZG!KNbyZ8&4K2Vn8n(Kp0R6r)q@1fsLk-yUrM1X z2H3*||YW0WyF1N0q1CbJeB!E`$-MP6Jk^nexp0oYUaUt zOB+eVRZkG8Zm=x0QOmVcE#7?jk=z7S9UCI7fEbE&X$L(&8&PwY;H<`z|i9zTSKOFT2 zAoBSDPURw@j>ck9h{zh5t~Ay{atEJ|Ry!UnD8ASl@dBFpv9=46jJ~Y79(bc$sh=cr z)=<25U!}0JuyLf|YSv*qcC9|$2N=!jiBN!2(czd*d5n{vjRwZc^q81oLLOL#BRu|D z?HCpa@afvLv@AEzj7*(fPt!Flvv>xp0?}*J=IM)3?Yn;Cvv}`AdSHn(Y>A)A5?Ibi zSI18JPj9z)Xi3zg6|59bit1DQ4g<;HL@IO1IFq;Sq0D}*wD2Fs84q=5Itps%Og_*c z+2z@6dLOS;LMRuP!8l#OV{4I*1x%T*T>`U<8HNgf2;{3(q^Q?Zv_>r_9rg8J&O{O7zb1SgZd_P^QRrM&1l5aFTd0rgtFj%{w@Hxwh z`b1&lQPhhv-tH`9`XcBrj#)0x(bJB{!@&-(X_KQrWeDUwj`-JNuvOOH@%935)+@C_ zuZy28p`Oam7$Mld0ss>;vdp=EAq|Nrk9@}BjF9PA6#Wb&>`4JykO`tgT$(YkE0Z08 zBKAw1-&Q;sjR0P_-@W%P4FWp`m+t-YXzZOG zF~(7r46$P4>t8q$OqP94JW&gW@-|mJt(A7#k)q4KjCG|4-BOwh{)p_e+Hv~qRaU&U znRhM@pR^mya&`ByIx4Ci*La%?F-={btx}pxBDm2zbiC5Hwv&WxO`eelE%H2v#IUsj zRUngf3Caj)KIK1<@pb)lt%W@nsXlze&)lqT-N1KXTDM?HOcJU7E<_MKy<&F7+To}Qe~mH_7Y_D;J*{hNc^wlJAr$LSqa;$ud9^-BrZIdrW(*Lg)RLd*-+{3j^+1j;fW; zV(GEWjpO3_C);`E=moL;1?ddQjCL7D(VK~Htc4E`i zd$MANf_uhY)+vvz@@UWT4}k**S}P>n11xU1aAJY^0)A%(1lk8#dz(xf#{K8lhoaqx z++18S{2@UN7)S5p=z54j=veZe^9PUr>$Mc@O~;jPCT|}g64onjng{O~ppHRYHVurL zkVYjnU!>>DFxGn@^eYOv2L!8UO^-->lnL%W`0$C|vjDG^N+{pPcZw+eBT4$G$ZvbX-hIkv3vqvv0G+ppvl z1ZI1wsep;~7~(VF2u@>#|3k-`E%OUjp<4GJk?h$8TGvJ3BvcsF@c_;iaUl;~Wj1Wy zYebE0LDCy^tEWJAs74{8`QHt^Okt?iuN9M%;(xK%Uh5oX=z^b0*?}Xsf5)l9ud$hj zcsk(D{o-5td&jD8D%9fKe1J732m>g;%& zN-Ms#N_|`Dl30SGhWCqt(|hSYuAf{6vT3O_>D`Wf7H_Dli}yhH+s~*aP6R?#+EH^CoXl71UkSd^<8Dqh_7LztBHT z&3}5+&HiH1fS8bu+PG22Sh-GS#p8wO^cd1}DDkGcENxj`AFqEOUONr8u1iZEtj-&X zKAD{CU1%(!GI!{&r9*`g5$xsA(`l1# zO_K@^ElZE42XnM6Gk653W9Kjqk83E_;CE*&PQRVPL{F8pANu+-!*r&_+Bl;{pdfG2 z;X#^WFBIy!IQ>{*4u)hy2R?@>Kl%?s5#F)fUzZd*|1IFw`@`a13NuQ|ifDOlFBjPp zQBdRGVhMui5bc0UqjwrNThcg27cP

?orbgYIdynQR^|`1|GTw@^_VC?B!dg-+@RcII(+9&}-YSabtE|6ArTOiDP|ytY6J=w8gbLNGO}-X|}S$ zaKv=FdjOJ8b*+0?O&!XqS;3YriwL(Cl8}HZpCG=`*k)xyp>hs0^BagC&Oc*%ZFb1kyO&BI|=}2S`@RlQdW{&e~(W@ zf0@;9gCg+lET6g{$Gph}ph{I*HmG(z_$8uUvn?yYX0t3n$hc%$7r3D@#vMdh@;OwF zV!I2PBmTLy^U1KpeBq4DMkD7GV@LE(7v74$*X4wL2HV$hWrsLl6dy{Aco8^2p zngEYQCinV&_XILiTr~wn!;UgMb$#DADT~8r{cR2+7cZPMx6I+arV0IpY>sPuZ6f=* z@JUixdJMWc`$h2^X%=URt|y$x0QFNiRhueWDrFT(U;W-t;|1j^ZGpm|;H6(HgDbjkTSJ;xqHdI7VNL@sJgDhtTKpw`I ziiKqXT(29=*QF+%x$u*%{Kr~7TMkS*^5AM_{o%@9|>ANnYm zm2ELb8QO$idEVU8PM~AuZ=?NE3wgZ`W|+mcT*`eeP?zDfk$~3+I-*2-yLOzdmZ!Nf zh}8y87f|{-r$20=x^NEIwmExu%lD3n0c8>f_8FCYPpEaBVNk@<3lZgdk_|ygiD#N+)T?oZ257B#D0GMltvYnX&(olvSNF@nWB_m!v)H) zWqRTN^{9F6&i!6E>{=5LO0>6ELEgUJPbUs2tU&mDh2)Nv%hvco1gcUQV6^+dm{&_d zIaf!?X-HKC7&rqZIGc}$+n;2H6`o=>l6$T34ob9|ol^Nv^^gkbL9G}#K*B!Hq}jI` zlA=2}RUlGdMM2sEP;m3T{xCYimQ3l(V5KgBN-&QmFUcE9@klhK3E7IN(C1BM1a>&& zX-LA;Uenq^8oXiQ#QS~ch_Fq#>I>u3M+wC$5c7F5`1vN+nq0&);hz&m5)M!ni^hl8 zsmBJzfK;UjvSdr&ma~ASR8o6w_FJ2n+ ztj-}BQIGY{*P2ko^~3N=OW%sRc^&{Y2nI9mfnwYC8~>hyb-{Loch7>m{DN6oMkc*e z2g(A!MfbJz?LDA$d2|v^pAG+rYd!O_OA=|@hN*H#y0>DI-AM?*Wy3Vke~g>e@Ho{Q z6psO=)(J`x@5=ljFvAM&y=7}?4cqpaahPcOH2CN08P*|tQW5^XB7!V ziqh|7NuX2O_re{fTY-q4;6N`BL4G=C*a6A!;s#+x88R_2QxEqOP$gebZ`szpG5QJ9 ziUr>j-~Us2)U2!j0C)eZ9#o-iEzf@jf+U#)n0Yd?__|p#RNDZ=QfCUw!u{(rCXb>2 zdk!-%TKKs~41JLFTz&blPbMZ>YByxF+vDu_k$BkicR-C@?K^xy^+52_kI_bR8e(CFy`1Y` z8DNk`-dh6&kYW}v)ubwZD(T)usM?}w+F;#hbCptSm#n&a0|k4Y8avFC4Brd}1C7&w z*dLCswLINB3@y5oL>`depZxH&o{Q5p>V?I8GNSPEM&~F5b`oP*E=xnThR`e$6HG6YoOG&NOrx+Og!Y`h=C014b`YC()_me=l7)nvaesku>< z{eLKsw^0dgnA+@BqWZQ8o(GaFE%=5x%9RbvgHgc7ou{jmES2)=pa``~qzjIo{W6f0 z87BY}ayaV?+umPY2Nj4>)=W@eENN;RTQPAE&~J6r$Z&zg9ngB3z{ktN&&`2?Ibldt zt=s%Q*AzioL-uD!PRus%maari!-HC@Z$hVA|TzFu_-Yl4S)0H=TeI;`;e)m^69ql1+EqX*`Dd@Nze|43FRwOBSjqYPJDfYA$_or{dQ z0!l3tBn>yCV?=Qp=6hor2r?PNpmwH4wjSLy1xK3V0$u{>zc$c|3QeHyzck`Xz@+K3 zT+!6bJ1i0eyR*siH&g-lUnP6K@RCG*btRCA^fjyOm%+;qKcc0w%_OPrx;>?Fi@h>= z0)GKkt{laJA1ZU;-**diP@U_vyRysh?m^&KoElJn1YJa@OlhN!{80{1bO@hh5g$a``gNZrq4MJDBY}j zii-iNKvc3s{uGfXR=mV=6N7DvV#P-|5NmR%LvBYp9Qd@RQTs!=WW6J^P&g)Ewi}I* z5{n{R*s@v-X?=}Dy!m1pt0n*0_4lcftC7!UtH|x&BXaI+Uq#*2qUeI+l1d|gdPhR9 z4^a`=W7P>|{{pd!efWda%Jxjf_*yp`(j=!;M!ymEP7l~*f@s&DLK~^UV|!NXJZRrR zDXve=Wc7z96IxPZ_%HbtlY=!6r`$0kfGmHc70V|!FNdX1=C{vcA(eKx|4QE^3)M&% zzff!EZ|l4B@M%d!B;}iUdDL3>Yrut1wbJ?PjE;?ez?PkSq#cLi)vke=I= zC8NN7fH!gEW19U|zc{0Wx>K8Uz|QyYc8{FwmJ4U_(IXj(@$EznVGC=mt~tCrp*#AN zgMi}dfzVrWvdS=B1l~z|ul_ydx-jAlp#HP%xfC#j_LFvOwV&mM-R0A0YEji~OeRq!@Y1+i`CRec??D|E~`U>jL zg2M)8iz|VkbHbRRUT|}8>R-u4Xa-VOzcf)xZ=D}m1-wl z=#88~O7LGG@e3Gi46ZRZ+xha9#6hlFPHPUYzo5og$FAn{Q`;~S;7K})QsaNTjZW%* zYSb#Fy7SIGjT2@nuz%yb(e%D*!U~VFVr_Ylqfcw;o_kxctuE|5^tBMwhR~N1pxl13 zW7xpF?BE(lsuH)U-QX!ME_Li2-h+zB=uu-1#Q}LgtrJHfXa}Ai>IN34Qzq~!VE#}v z--aOJsg)wLhM8J8sJ4^y>B6*%Yvuh8%k$ap zRHhPCFeL9~tbMK;<)*EDCddZ^Eu{bw=6(8~StX&~kCuAf*N&azu5r~L|G&AA;MJSv zS4a)se;zU6e@{^3fQ%IcFQK<&rG=p~tbYptVmcQEWPzOlc@k!QH%c$;qMYLRSjc+k zOb5A0SpleU86J35X~v=Ovyfx2UCJ$lYEVJ#_PexL)2-=_B#^?lAfJq^Ug_hQAqY^5 zGF_>0EAL#Z(xkQH&N58je0dkrO(_sbLrT~=DgcS3WMWTDjfe1HH$H^+V{iu&iVOWw zs9P;%pwvs;0Htuv+zKjM&_%Vy`7-pXhy%ctUsuE5jk}m^0*(-s1U~)%TdEL(;+S-t zf05az&Mc&(MQe#=w!J_+Qf`>mH#TizXRaB052Yfex9^8rvb*1hJ-;N^Xze*uo0_Z*J!I&vDAw2DarbL3A&w0l(<$1{uOP?O#%GrL-B~xwdfr>0VtLVKPADfb4?kpD$BnKcV6%4YMHO|=BJ7_w>5S{# zrTQd++}%kF-5-wj=kP0KGoU)mrcBtmst+!{Q&*c{Iy^Qq0|M@L`U)uSnw}HD@j&%A zK2q5BX;3+GAs>`=b_N2mdTS$#cK*=K&ZZ*{0ucvT^msu;MTlZ5t_$K>NOeEX zqN~3ri5olpUpu*uiSYG6ZuK)o3sRP5a zJ>H`DflBfgE?Myu8N(nGQmMOnJfs_ff~dI|)Hr2KU%9_8&D6Gt&~KUtYDZn2YN0Ao zH;<5RaEt1|wf8vs4%7wHo*80Pw?}=)oZntMacg&V=S4vutn2x<%gzL?hK`DCTvI9W zY4QR!rU!TNEvJ{0>t8?>oZw{4O%jqEL_Jmxi02`IqGNe%YP$)@MyQAYzdMiSQP^FK zG1tLcDSsU~7Pk2Qx6X+h!gw#~#RXjhBc+3?UB2}h-L=NlA`{49Ol803!5lj9vy-kX zos=VqC=emw-2E7ZkA-CJN3FfAsyOhx)lU0N)y*YDt;-Ke)}|V*j=HlZmAzbi$E3=I zunb&*h+12^v!Y+_aEbbask@)ePpdnY=$$Ve#Sq;vuXlpih zsZ&RSg~x`^w4Ys(qI9F6a-FflvzD%xIA5z z3GF(s1`H*G`xf76X$Ct{(9!B4E03TfHJ~;E1TK!H)>Nn@)q%%-{!jQzGq)USNZ|bV zRu*bgy-A%qN!_BQWyztlEJuW@q(-9f6)I6`EtYwQAADtR@Fg|*QXf%rB!6y(p$U&? zkA90^b%-zT$20*i@1~ZRh~6vfJSQl)S5^zs5g*tUSd1N9 z;0I25y|JR_P->=E7uQbN#{v&40E1~1guz4m5QZfD&Mn$U5-B1g0?(LIo&ve_G>l|W z6n-zzFgetRz|!bxL5!=A-mPiBIUFdKWjq>n9KGnXiB#S@({+N%1O%%Rn5zjbgib`& zgBdk08;|tCX)pup^X@m_<(!GcL*5b@C~@opsGzLA2!EIn;JB(Ogn+4UzQGi2?Lylj z)e9?q0Z4eN+;{B2u@zkM_py{A=tzqu*rv~|_4uB*CIpd_ob8jG@4V_`RpjoQFUE68 zgME3y;b!}jv)bm|Hg>u(-e;2F)*@uSTFB&~;CkLt8Z-<+1qu{hrM(LvZl=n9~ z-~(k_NDv-ELUdj!!G$n$HBHnx!9fih0hM@y${Y|wR<>sgEbA1mvHwB!A^2`0XLZj) z1M5>Jq6{lYyaCRtKm@*|H9qeKf)(=L37bNRQYbu>4XR=Uo2+iKeeH%0$WexyL^J$g zgGJvKL4q!+i_{21j_#dxc$!m#5ylHs7NvH_s=asoQk#hCO61S8q#5W`LtAPFv!#}K z1F-&ZOKM&nkhI=#O4=v1!7Wa=7RwA9C4~?{63&TIkefnG*i7o!5r$Ufg8f;xhP%F8 z4on0@d4sticGA`aA^#aFxo*>YTn`mXf?iZ?e*!oFpT8o7Xv~RJ$YbIu2OxWJc>TKX zKn@ku{zD=P7g?AcY0Vr>B&Ps0Q!vXOX% z9NX8uT34vSHq-h{(K-F!KmjH9Cb9GkIC|?Ts<)lBoDLWpdY{$4J=#Cpx43IpuFIH^ zR5phWsEpQ?5ggvg{Nlgui*YR0w$BRtc`!`?jP?$hGp;B_`&SXtus$AWH>=me{-ewlhqu6-#-OMzx3X-)zH(y@7&0>wy z^am0hd9?q@RBxkYW8W21GQmQv3Y;JjFYD5k*`+=qHG%C0zF@lFUgo^Tt3Dmn3{*x4 zTXQ+9TLy&1xQK`(c&EG{9TBu%4~`G{5OJ|IKXdlwhmjFm05)=DpbQ-dryulF&<6ZE z1h897NAYYH`qyP~EZ+DvFR15Vvq6EEQq3|adW166!DBzgpbs#(NFiGRuEXAUXe;;t zm$%>ytd2Q7#|n1=oT3B$nr!VfC8p%$S>_G8{GQz90p_swAJ+6N*6h*;aRv}H2($n& z$UXh0ixL*6A!q=|;6X2TW;(tYEcy-78Z|3yKbW&P^Ft9TSm}lXHzZC%Y7rL~K7HF0 zoGcLnX{A7N&m(wgNc1>Ak2U2Z|y>RgI*7UQo1;biwUe*NsOs~QdoLQ0(s?n z6SI#h;Ad!UTUT=Iej-DHS40GwI)}!Y!mdH3^r?6}=&>`CWdlh5YulEM0-?T(sQM3p z*%NQny+HOB;dQeEf1Jz=og|fGsH!n$9k{s2=-r*5DNzOqJ$(B@9&fI&cW1_X%U8v$ zzh-#MQ84+>tbcqjz|RB5kl%&mrb(bi&SKOK1Cnu5yG}Lfgwpj9j`j*!Vz-D`N`Rhb z<4wj~(j=4<6||Y$LJyGf;ue4gDIPDUNj9{8#_|msA&HxmpQ>6!+bbzRo z4m}pjTi}EJ9RwOJHyPV+4nV{DAe>{}W^W9wgBzSQjnpm;bNkV}r_(Ngw)!&8i1F^H`)PW7UzkNNylq|^rr}lwZ zp|@AySqe5~?i!Y3U8LD8nwt=+Z0i9oxplE((PO9suis>(*$5oh2yP>rI$rC66tZaa z_5xJYH;?v6wp$gCLyWzmT|>}!g@4^2ema#}3ruYFBr(doB|lj4+GA2KIX59+o--KT#rm8? zrz{TTP@|j`%{7N7;REt`J>Of_pk^4>V3@^RXS1n$z6zH6HiuT`cW1@!=rf#e;Ig%z zft{53Z)%%Cj2;RSMRJLZ+5O>4U_{-Ls!*EQ{FW;zF1;9F?ll{-GZuqOqz3GAqf1}j z8=k}zb?*31#y>I&3IQ!IL|?7{wsIGi+hi2@M6b4E4yb8CLiNSaIh#SB$65yWy2Y_F z7hF`2nblJk6Z*m6o`BZK@FzGOy|JR+n!k!WvbL#npvl(0cSvixDe?3^@w({qvYJI2 z5kt=${;Fi-FTeBA_LzxQcE(nA76bBYy>}+|>!ZE5 z$5v6>Aw5qc7=ITi3+}gOxID=iz3#B9zI7cP@@~x(+6ZN{3mjll%8a7Esd|H$uG%v< zK=5@Q9X;Ic)oDLOVtaJub;9A2k(>JYyL?^r-Fm^@3FofgI`d`4F9=S0kZMG;{a2q| z7Rwr)N$SDh4U~XP$Nb9x_*Q~%*s4OB%$A#_d<$AOl-F4NN2=t0_D7!@65#UXIiBI6Z z1#MQGJ+49olu!&G>^uBT<$MBM+BKa<**nO(+?OB&>k4#=F=t^eiod5u>DUV~WeXM^ zV7Z;a_g?2TIr%CoMa3i4puOo8X}Lb+zRkC^tdrmY|IfW%@f=mEXNnrC36a$nAcX`L zihkgI#eb3kI@(&W$Qqyar0Q&eNpZyc{{Eb~L;T7^>!Sx>Buxo`zc2T$?`!M&Y@t(* zTv7LvLe19`Kx`Bgb8>#$jl^yL#~1Al{GcLzZ(0!Q5qhi*n_0>{Q;USI0K{v7eZxr221HP8;l_{so(UCP+h zW_JL6;k%pas;c*u9A1}sj_imC<>r`bahHQDpHgFXF{(c=c>}C9`gw--wdU@#x}f0Q zGI(~nkidcByTAVys`ws3X$QHu_esYm#iumDCsXbetN7SU2nza22t@6^>A(i=V4w18 z7PWO-Thy0P5T;$sg*|kF%Fp?*yOpQ#Di!Pn@t3NVbM*~c`d%Zrs)}RI9SU}`{GeIA z)oE%OG&j=c(w;QiY03UTYyO0<-*bVd1L7v$f#tQSyBN8j19-GvHuVaqr3 zHnfhi*3Y+uIh+#H)a0mFb%*E>mghgzx&lnzyj^irg2XaF&ZJ3B8ih=S8aBjJl%P*!p8DVOTOn_^;ud zaF6BTJKL$3PyL49-C74jHTfOhlBKeClLyu?aMW0I(|fS^3(L2ed;?4Kfc-?<|@56Fyc@D+IC!N>wDkQ>vwmub7h`( zo=&~xvERb?_eFYXQ4V2$!0)-caP>V?=KhqePoEn8=<(Xn(e;AGN!qp9Lq)4~DF{)E zI!(=^Y>z&5-u~2-gL!Xu`Y6)C%d#xU@7@cCd;A%xdi1p>!sLC2>q7aR)_k?zK#ajr zzc7vqRb^`1OlsyJqMh=q^_AZ*;qS<7S|gW_&e#8 z-WXE<9_kkd{dX`OAJzVF`)o@E2LT znEJgv=Eq+wk}$sdS)&1FdswCPs55DqFa8qacJO9x+I>?0{YMd{`{{QX&c6FcCq@-S zJhzc8ei*D`_O&p%154=Ty~U`JXIc4zG4$BTg|GHo2vslHdH?$er^arF81Ax}k8qwO zE@PPkRAKEnSJO&)`9gEPN7>Fu3o$%kl4Bb&Zjx_jkb|eE2^>2}w>_HzCj9@vtKrId z@4$V@XyvLHk$H%}sOSl1-dQ1aYePl)j1YdX?GUW>p>*#Yw7iWmmR!IW>Gtp|`Svn4 zMOrTiP%E=8BlZv{%8Ld-rzoBuX1D^JEo>F8O2sn9YPLou1)4Y&+;kS`XY6AE4!LBx zQl)(YmB1a}C)?fr_@}#LQXEokn3ioS?d|Ro7(NBS$R%S57FxLHg_GT05O?Rgo3^%n zE?Uuo=QcP+&h+H&J=?_Bx+-wTF>cnh;GGUq{3zSLtQy+L8x{Or`Rf6o{la_gzu~jX z(cta-@$3A2YPsAyx8U8{>X~hqITfT@TKIzPP~B2b0SzR{Q_ zIYC?pW7JNj-VrUJBlwJqqRu7Ch2QP9Wvz4aJZ4ADsd2MZl@(0_8=3Zo%YI3G>(Z}m zixGuS^GP(ngzKq^xJ&ezsog3cp#9udy#;$L%;6TB`5SPL(fceEn)s)#&vHz^yZJb( zK-NN`3$9ZhcolW9H4**3BBBk9;#x;LYWx+HggHNBQc&HF^YwO?4gkC1B_+8Oj)%ho zdZ_SjGA6(&?8F4xE0!PjNBskyZ)=Sn>+Q#k&=$qN6&8cy2?A+4W0QJMh-zfvu6U!#D1XrIkA<1!Zfo%>u;qreK7{bdB0JG#X3oEttwG0+ z`=;U{R;E*+1unokw!Oi=x}^WO|~{bknajuPC} zmP6cqF*+CrnZn#yET)!w^jK8q>`-;!yB8?iED!HlYfOO=y;6zK{zW_9yi>&4Qm6%A zco~1B(kpq-^cS|nbg3Q}`cqhkg*ohDgH``VQDrVKI97RimE$e_Cs)q(fMZXYOz!v| zYd-KRP=^F?Xyv%N^kOA6^bS_rmvvh{EfqGPq{?T-@t0#~{Qxe^)rsJ@Pbkvu5{NHE zwrO+ar$~5c5xrPQae?K6Z#jDw9!&{5S)LOPzU?it=XqDE=eKE$r&F}F>&;YRAC$2; zk~8O$fF7{V$x$ddeiQ;~nVPPl(qP#`t{J;>5C=UX*EHz~$rqnGf7jx~80V}q9%Yib zcQH?9bZEoD_5_R5lw1%oWg7K7i&ct}J~Y_-SeelV>-RC_y{qMF3S< zwR2C5SnCtm@^!SV@mx=1NCUk!D64#jkar_-c*SqPhu}m|XQzXNDz%X6r;gH$Q(KEy^Pzkl~e40^tBh z2(6qPFN#)Qjxh@|6A9e2m)zsFt3xg;g4KCK%|CZ=nxc;5&{T9=uukeNJ2{pMloEM;@zVaqCuV%$i&xwA{{gu7ApfYx47-@%e7%Haqz76R)>QA~@Ab zZWz&kxzDXPtqu%vo)a4V9$&p&MgBNdR4yKG2Zw-x8~CGJUtSYGUY)>#(HKoe`=sfo<^c5`FRRJv?FJi(8=4!HGv+gv(FhD23eIaJGZ6!78 zHiAeiWR!B(8#xGtL?9pv(^MtDs9;5GXVNrtS~TfxA!}{*wO@xRP~*W9;7UOsgYv$Dl&Rt{O+NO1=o29Q6`uNh^lybNpr9xt3OC9 z_&<^Jn5;d=uTlLDg+*16Sn z+Wr$urf16KTDLDJ#SpK_`{u=(P>mnpk0i?(Pm=#E%Y;E#LH5`^Or5?5PbH|A~b@IvdaD8@{sg|^$2nUwN0dxdj{wz~j! z%A@B14H75!+a3AZQFN(=CodbX<4ly@NaQ0uk?pm?!pTflB6QcVtq^|Dn}M#T(l>)~aQ4vMfq#dt z{QOu>;JD`vqH}x?#IiOPph{TT=qiJ(z1YL zr%niEPv+w=*I{jx`ww(mB3Myx(so%DAYshLgZJPy01_ALC--Qi-rjG!?!*bFaO4Tq z(GQpMwx1{74573`XjrEr01!94zzIL>l+3fZCig+JT|C1Xp)0bbSSbTkbLDVAhvBLK zFg^vti2cJLDLo2fHHqOJ?SL9FsCZ~r5BM_oLcNDMsFS}x3+gsSEa|4hgi^BV0AuLx zH({eB3WMNyr76+{pvZ|-lGe%EV~M`Ii0%vpqxF@c5=WP2h8fP2cK3?StKY5-kLR}j?y#CLQ|Y$e(`X9>>#=M= z1=)oMCna$kbOirRgSGGnb=VA6Icg4rR}C&Za(Jlx?&p^LXMeccVhaFG)9mxkCok6e z+yVX9=iC2?H`LXq*^&$NOhMqAs^`jPg_zEP*^o3}pVtLU`c>~a>b??F<1=^v9jIQf zdaKWPu4!0k>2EAB7d3ax=wHGZ zx9Dx4p;#Z-fX`p@+HGE8i|rtymBSpL@g{frns4-D2%zFeB@oSHYQk+ZaWm(dXf}yD zzGoBd2Jax^im|eHa`g0zQpCPuCDK0XyEf&cnWBkcr>j?}iQ+rDSKpj`8n-;3_T=<_ zhNsJY$GsWF`0fVSPmDkW97J0I@b$r+XJ8uQlH*F0Gu~3ml4^*+Ly3RqKyxcATCPlM7@^nI$qH($*MeTM~@JDag0*Xke;ldAV z%pB8H^!`z$x(i0sc0qgP&96zNMj0!HD%@a9wbK_89S#jf9r$*A3g0a>4MACZGv;f6 z(C?|Qp`KUv|G2%_c#ikO5_tdi?F8@j%^n9y=P1%K8Joe^(yg`!y>F{EbHWkGOPESN zj0&i;mgkRG`XQK zj_2S3;pmA&Gq;u0$McSw2^C)tDN2%;aGLuDL2ePnDn*FpYln!dw-_VSn&3w{PG8w3 zJQn~sh+(Zysq7oM*%4#0x%R&gzVnB2Ni zne$j2XCME&tzXCw&?MB`_>=#Y8f0vN(4DE@&W}#c69;(!YC5gD4M$!8!<<~su(gp_ zg`=JqWbaoT4l8t{Nk z-eWROZzN}JJ!;X8D`)NOQk+rB|c{hv)feny|(;5D`YooQxfmugd7T9yT*~Ye(L(8m$ zO=@D<^_gF=5)FJqoD4+6GB8vR5$c2!K(XvFKlpk3m55iCH_4^!cJSX`*B`y33eG1w zfI?lm@n_KxN&y!IH~Bipc83>ffvj7ba9Vu<<9mVJ>h;&`M#9N_X1)On)TD#mVj1i9 z35(-qm$x-S$azXl@LSDt0?5)2YaUI?s^C;)CH)(_LPf+_VcNC@CGF|KOj`Ke5JGt9 z``2gt|L$iY>>~-7m+ZPKKYTs>1c2=**0nyy+h2CSOq-Q1ja?5R=SH*k-4_zgqE?4G z>ep_3Bu$7jzo(JiQ=@)ONke2eLnns;j`iE^Tdl|OXOho0QKTw8sM|&vJ2ziHOf8K+ zk+DL&pU*qL6db34IrU~jyaSscgxbDRtPKz+w2W@tW@*$9FG!R>3Id=Apq9K|6RQl) z*AJ07=-R@Xw?vMjy;<7=_)e9$zBp!Ip7T&h{dG2hCEwT)cRIgSrm`n1;uO4oon=lW%*o&+X5wp+!}L>BYPvJXP(z*}KFrn2j!NpQf~X$D7Wcyr%fxw8d=64t54 zEb|Z&HrE_rjpV5WopY3DtOXD{Vq$o@>)wBz9*h&)E|S?g^$=%s5fd;UXYf@36F{T= z?TtT(T*_aU@rM%cAu_=6-XgJZ>0kKiymuTv!uQ>W ze>PV5ahd$4#+a)E`g{3ekpg9Uk|@Uq(2gnt!z_%YVSklwMAucr#G*N62!&GGpf_r^ zyNUgL%l{q_+SSm__&XQif3JoA&QBTZ&#p0lu!1O;z`D)3(sbGxg_~Xw-u@+5e#j>> zNB>XaoIByRU|X?g+1{)`WUo^3JZOiXcRvBSY#QLQ8a44eMfQRx{m9WW>`gB)b3bL% z{PdJ-Wq>+4)^hC!;PKqeE2WDSh5&!|_d8zQ*6Ct+846({*XrHjE8X^}m^e>BK3xIC z(|L8tALVw*xJY3eu%v#ftVOd!`yS7)c|a5zM6~5Fy5$798Pi<;W^b=sTG)4F<5{tG z2?+B`iZieOb!l(ySsL3|L!vNl5|H$iLGLff!3&*42Al52X_lt|{01p;alG!f@Txy* z1AeY!hDZ83oKJO6v*t2=hxi=8Cf=jz8JOEZ?w0>;teL2K5`{3l-0$oaTuh5Z|C&eX zEJ2n=lH_;AaeDUR&DA zrTe#lw*#qA-8(}WI3)=;ezCg%rG%lx!ByLRly;m@%e!Y-|Kw&KYryxu$_qF6=+1TS zix0{hXi)Smh5tZX&`VgZ&HS(f>0qIuTiwXO2TB{T84m2Z8?~8Gf z*sY&-@#(Xk^#c~hyEoP9Y8*|P7oJZhZ;r`I@w~f3!6#~>_#*GvAG-;vd}ig~wbL=D zZyYV1g>$s79^3%9r{^QP!A@;QIEFNYtbapWZqY#mA_Eo|J9+i62{8mWVsLxTL<8q> zK@FrGeU=2>?Dn3NK8X)Naf2*pXTwFz#vVxl0is5wdFsarK;xVTURMmLWj_iEDsX-~ zK$Y{@+3XSJZeM{!K8Lf=Dr4KJkL$p=n(vyJo8}PgjeYc!*A2yM|D_vdZ#!XL!~C1-#_i&w zq>k`%KN!J9$OY+}XH*dDBv1=cQe`*qQZPr^PNnB#Z9ehw%I#sPv2c zdv5m;cKJ_$Io8p_7{{9yl^jUj+&brqHY&|sRce8Js|;%{T#66FJStI&;3k-4ZJVRw zvmzd$w^&+Lz%Cok7G!>fX8s744bXX(UaVw2V3xt$vKNyz*)N-wz*1y~?b9GQPe$@ zQb9r=k&ki>@WE@}w(V8vqusF^^@!1x;hg9Uv%RE}(Bd;n%cCmB+zc6jx^p+z!4}N+ z?GqNKpw0Km=BFg6R%(tX#ah$nORsFhR5`ns?k^Ax_EKlEAc}HxBSJkDdFic3E4DNY zmN0&n-1EuR93Mk?>3G1byv2e-h^d!y!Z_tjjrwnYzOUjhyolUsAWZA6{L>0cK-dG#HH51Xk0G9m7Qc5vBzIPUJ+U!UbXpsJomaf3-+ue zDSFR)Ggzp2m@y_g+A!bk5J{|t?C0Nq`S0{$|L5BZy`C%gbOxBh>?oFJf6^hF%<%QY z`#zqY_aJpavjq==W#)=4A)xX?5htWBgM&i2*r$@IFoml~(YETb(8-`o*8Qg*Ar2j6xGHGYKgagyp|R=M zzxh@@5hLc7&;2)^rQpX-!6?*SBkZ3}(ceEon#R4n#R7fSeLVz7tb4nqqU7mUvv!4n zK_oVPi_OFDZ2-O6U7`UomWn?}k(-O?0?~{A2g%^3UW+$;i5~}QoWFa6cdTlz)XVbo zkNZ!Bf{Y}?Bg$?QN$K(^FmA{*LqYx(m{ePE^eqsH%p}-zp^rTd*>@INFqnbrmuf0| zW6xdhJofFPSlSivc1Uc~%*c&8Xe5OVvQ5+}z`hrb3ycsOTjq>!gy^cNgbeJRDY*uO zSEjPHs~};6Hs}Pv(EgY*s)K16InyT(s@jx3%K;5ZjWx{&lf4 z*ZImiq^1Dk+-)h%3Ci8^9hx!s#B$!6Sf&#j!cgrczHu5~yt=+YX$YYzu*0S*KU_AC zRb8v9eIk29^EWcsWp*X#+ram%Cb$hb34_9o0fs$J3>C)v?KBgKXE1VzCp42%_#=LP z{tY&*#A<@l(~UD{17qO-lxb8I#*9`|8}*?|qLKf2pQ$?(yOr=FT{R zMm?WaT>;dYvc+%9Mo2p-1!}9a9r_-W^>u2ks2^DZ%2T}eqV0f^|BoRAW0pU4zu>(iR#;Gh5_IzR2F2Mia8PgFeEfQqk zS|D%a2V7_Z3uX1Ilyy!kI8}C}X6COQ5VBv;os|22E^AUsu&3qNWdUuq(Ha#X$$6`# z1xZTTB*GM4K`V9O*tb)!Y#W!%_a)#yWB^HZZ><)!#~X^B`;rR9iV#l+0=_5HJ95_a z*(bmoU0zYB*)P8UQpBDa2ZsV2lkR)j&1&Xc;OV0`;M%RlfJvVbV5n{{xiG|7V*9nI zbidfot^Y9jpyMVRn4sd+l;q<;J3}g`Qh$8oR(W zi1kt)oGvs2!l@w;3j=0y_zWBR_aAUnA*aDG1offagd3}amC((_zxJd#b(N6`$-eh! z7e;gN>Ar$863u2X^?s0o8$V!Z zgvKtvp$W)*y{vP6@pjfH+&D{%F>&*;4A*$Je=}Yhz`> zwUf_u2=R3wH7WT_^SY;HT;^4reFJ%66BM4p1VHK+lvoC+968}y0)`sBwZrM9c9F3_ zgMjQi_L!oRAzV*S4rFI&ygvV;EZ2h^R`rC5g%jrr(o5UBx=z)sj)7_)8UK<#=g#S!x1JWUogZTL&=6JK7 z2&%DdJyTT~{kmy>NrxgMjw^-1-$)WD>*X4)7{PA(WN7H4wJN!`-c15rqyaECfVUCk zk9kl*BSWTiMiZa$NM8tbU!#yZl>kGtV#%5-HalIVIxQJuJmJ7W$>-k7t29N!umx*w z>&6l6zk^6llGwTsj+$YB6YNP<1u;wr1s}{V3e` zxP5u-*&2p->=B?k^g(w6LM2uc&4mkt5(zX?UJ@+s*bhOSU9Z!C?}74m(p6>LE5GcS zEQM`U+5ty>^j?=8;&C&BsB%FfP`(FdS5X6Q@UxYbBe9%Mk4hBmr_s)-{y4+R(D>)v zyc5HKFsk>0=|cgGudJJu*XN^d>pv<0872xl^#YqwEdiwlf!qw2M{{u1d^Ee=N(Xn+ zBF7GHqRMwHB4<>{EW@=2i)l-O{#Xzu7u^!ahVtrM7Mq~xTf%1@qTW+oi=8b>2^8$v}!O~FZ9AqrsdR~40@Sod7 zbz#gJv!RJnOf$e)#qM^#1<2#5uDgIO>m$ZNN5q^{a7lol3YllWZxnLQ4StV|Z8!*$ zZ=DA=(*c_4+JE|kDWE!D7Awy|kg)fK`-Y!Z-dc?|w0ZCd_O$aD8l*W(Erx1ts`{w&MtSQIJNjwExo(@5e-**uVL@PC5 zeNc7+Q73J2!FG1f63A4KwPHv#AmmZ!h-lL+%1+lol_L&^d;mExhu=8_82FwzJlLn{ zPHFPFQisdDeQZL&+K}^?jI-yk&Ac0lTT;Ny;9olWu0IH2iH2diQ}6P1+avaFq5~3@ z)IABqoFSKSw}VnpnfESc7-;4jJuZ}HX~Bc{Jtqdmh2(2Y(O#kPE5Gb#H2JYxNS-$Th`6^{vU z0>cHM`N9GqPC-pH(sUX2FI6g8eYLlP?RHRl_YWr^# zv>J##P@Y4KLmf!+D_U~wOeKk+bi$JEH2hpURZFGPPFIQ6VY>}unZj4Gr^%;`RyeAw zsW}_3X@lQS-=|ESer%~%yPPX|#Nx5%h`$@2aKHy(j`RFhfKLJflvtp4U>O#a3Z4QQ z{WPARf?mEM*d;)6(xTvm#gOxI)A+X|`l6+D^dF+ANK^CEx&iF*u*oTWDl|Qhx4xQy zDw9*BiLKG(o2!|}?T-_mvaE>X#f2XVZI3=YWO3PZ5_AfnKPs1B%dc>zLAE49}#MkX%$D7}lSyW%E z_P8fs*_AM;tmpI&_}xm`LDTOaykBd{ZCeQ#usPO3%=Qn#Df~WQ?)`DXkkMZuq66hS#O3^4r(VzaRvIYiGKH_vzy_(7?;uH1seKSJ zF%Qz#f~!J9L9Uf-EG!3hcgcv_$CMCQDH^mO1MP$&tUE1BaXT?Rk@3_~5Pz6Ke4Tqb z-;{uY`sVi=fWBr>@0=OA9*eGEyjjRRV% zMC7_-u3Vmd+fSVI89Z!h1#?B}&K=a`dCc%Hh0xx#4dpg{;mr<8VrU0NlcC5I-+v*5 znb8wsq~qCDM4JTAPPaVJwG8an4-GEw^L(`zq4#M?4f^R-rgpDA1V;|OL1`X84-ufx z1ol9g$566s@_|!jSS>`6x_x;&s$os7I)KB(_){Cm;fw!irce#W*s>{n9X5lC+p2%A zazs~zF)Pumt>i3=%(z4$K5QaInn_r=15hGp?sQX_;Kw!V!y%5?H3h3*e&_3J2 zR1GQr5sBb)X=y{Ke6wSjV+TILiedVv{yKI!=cz-w<}tKEWv9i0%-BHdS|_h51bg-r z(QZf)wQqP%c|9)b0p!#$BLH_CirII?F+5<;0E3~5T%`*K?k+|}b^4hh&`S+Jvd;6K zkF^KcgqQpUJBhn3^hIK&`Vj4Kh97nppG~SA1n?VVsDg;7{h8o$Bos@DsklpmWAfno z{$e$GEqc6{3sji|XAvA`ixY>+gp@zOqwaJyf?_+0VP^mp3^M+Xfo#r46zxQZcm453 zn`MsWZ%2{<*on=gB|o^-qz{%!aSAoDID=6h#2iriAjHn*!~lIZftqhy=|UQaba{Y^ zhB=i@6Sj6+?ZMoA-dSTwSVUeCp^^-y6JmbyDNeYy!6ciZk7KLPU_Sl>WcGl?k5KRf zhBRiyW0rB_qkQ*OaRFETkhTk zM?j!Z1WJjFbTJY4UtR@=BwH~j!7&CvKZE>16FbZr8O?yj-45!3YX&)u8EP>0rIJ9& zl~iX0*^{=`5Lz)X|A&ePJ2{X~oJ|#8`OkM6I&gVPUkT3ow-HU**%Vxhb2U$4~^`eiCWC;w>$N?S>;3YNuJwt(KOz5Li!=V@xIy zE1B%g#0F}&ok}!pPb!1_A%;||Dvmill|U$TGPdnvOaR*Qyml+P zB{QZFTqNkJn}B|wMfW=(9zd-)KQZm|?U{F+wl)-96#Eifa!R28xl$`LA|P@XAiFTW z(Fu5GhN`Eex!)?<2TIGN)9?n8b=E^QQ-HkY&`IM0fkHPgwLFm~D6CN5Znl)%+5p>H zEe%w^ACR>*bR#38y|fKK@#hnz;`Q0hfD@P+_|uk%k&$94Da6jl7AG=7w2t6hYfq~x zkD!j3SXwH?Ili#E_xD*Dr2xFy>Oi`3peOFRQkm31FUWt0bKf^u1x~=2`H57{{NyeF zF*FN~X*c>36_*1pSFcgOgIs}95Kao?^+k%`<$2}mOG0B^Edx0lCK`V&k_Zj5Iutfm z3R*;YBS%i7{S5s*l-GFQYtH`%2rZwbXfG{1it1mDdFs*b=vRaWsfOt?ymHFEFFMt~ zLB|OAt@IxO27Y;KvzHAK;0!yKbP0zh{YuIDW&8HzHIOQVf$j;acoB!ydQYb2mp7pW zPT+4_fy5RdtS8rU(EU^BvmKKj)SYsyX16FfI%L7|?IdpIw#$D&S-|9S?VTT?n1Vdt zgOBCXhUFf-G3Q*hag%MGkCS4mA^3HVNUW_U<(RIV-hNp3^VATK`mF~jKA5qb3SICK zPUy;^1cwLqxz=C<2z!U%FS$e4(4t0lx=89y$^!*bR#$P-($OJtO^t zMUJbpr<MMe-uGy zM4hh9_=!g%MTJ1Uq|*Z0Z74=`2d0EwHd{YrX!8}&mB>RbtCQB-uWusD}SGz zt@49ND41SyOYMh;e{`OB4^3ibXNEe5)<{ry)F z4o7`{+@{OCDB~}wqw&bgkRW&Cc7>kqyuT{*PnRZgYON zE2DJaH(Q}BmIu`yOZ1me#w+iv@K~vc8J-S=MvtY<9`*m(0ziQq7Ru*{Suyt}$8`(S z__M#9?&EUGFp-iXzP*2R5PrZy7Na5cgD0tdrwpkrw3LJIM#v=rRaxuXE6eE>z8=U;>W{>(_l4z zZujgyptrXS(qVv7NcX?$QG1KLoyH#UNAIzc<5G;ZWej(l3OG^;<^%sitp7HM2jZ%Z z1%O-V??4!uKf+awQa^iT+Cd51{FhBE)~zX=E}i0fMZd+uteG?0k|A!vR!{vLAOQS7 z+?ySI@~v2yA+7Fb#PCZN`j+A z4x@2Zl7kH9dgi^haVhIeQ*;mABCAZ)QKm?GEBN|<^~rnjH8wXIq{8UQB*WTD-U3Gq zg`qa8$>WV;_Vl0qX6TP2S#FYR0M-8gv9CsY_xRYsA-!?$Kj~&K|G4V%IJM28teF8ZYU?X3gz%N3ZU< z#lX=!1;EHo(SIH34t68c4 zbeo_=OeK(OyRQI|{eS&+>Tw(b&VVXJJAE#v5hFIWw&>moQwg^a4*$FWe(&`G-B&_y z{iPI?;>{lK`bwbTHPqoqvyo~1SxQbcjQj5wJ_hnfrBjJIOvod;YMZVeK81dt484C? z(7q4lf&&~wiryV4CX(hkjPyS*6#HYTpf?2^d?&MMC`79J6`XAnB{%c2F@R@_DU`p) z+t+L1@R^sQ!UkXKSN4x4(|X%b{#i}dL!f_QbgMGOvwLf=#T1ypM=Q|;-SYZ02`r{%OILqhWA`_U_*ss|zn?s6r!?uO{&H z6brupsa2PWbD7KX4+tDkLCy9a?(;QNUjOm!0>R-d6TykE!tn-#6r-T4!UcjG_Rk45 z7rT5W3`c0=)!SD$mg2%ATK?2-g&{Bk6kNX+qO5?pwYP&dI~*(ClG19~Z4$(kmGz4n z#H@Cx2>48+~Ld=Wa7J? zbac7@Yb^P8vK^(!I56PCH!hviF);SzqW1x|$;Z^UgbMOM}NCDhU;kUUo!lcx=w!GYD4-ke+mIP$8sFR$T3@`Xt^ zkA-ph8c(?CMsl<~MHm(ygCU`7x~I8_NbMdw_LusUG1@z9J}9P*!$f%pkXIJEEld{jUbcSVBiZ_e+z3K&436F&;V0T-lWbG7*PKtdFuh8wz zegW22wBqhG*9^tsO;^7pX5S=Aszsrs|NPcV!N?XY9|GAF=Scazg4TU#S8z?QmgYt9 ze@`W5de;leruyE>@iJ8*bw&?9p5clcRu$ZZxlhPo#k&{5jFoPW-{|l5F5_0{E@r*r zRYRXC6i_W5;{1r4`6wr163;tSau&Oi@mo(7~RbLUq5VLrD|hunU#>pc5(-h&W_DKD-FboWlK8(DEYMB zwPe^=X^QruK6_*=t)p1B7K_@lktK#_5BdJcRIbEH>pMN#w=eC~``~5F;&ALuH5x9W zvUwP;Wc*?XE{mvI%fFH_Qn4Q2!G?{urQocvh^#@S%14iH4BAoB8gV>6d#-V8X0z8` zWm#b9z}VDg1>LJJ6=O1^Zd{C~*~n$}(MUV8_H>s*U0xr`$&5~ZevbOp-YXenN~QT? z7}}*qTb!H>ST1Vl!tKp~DFZv;Eacq34P*RmnnD)W))#~+bY(XaHW4)@_g9bf)i@0( zY-!syp`r6ZrCVCJx!8SLi&X`~LG4gcaXpygQVnrI;15bSni7`=%)7Xj27lZn!<^_Ip0xSM z@`Z7D0&ECuU2#f1IsRr&YO2g@$z7X4e*{mLQr7*Dl#F!yK>3WXd0*9R@Zvb+uW7f}6j934O2qC$<>BQ(0JG;+S)W-cj`_r;Tz>3kA{w zcpT&Hv($gg7l7FBCwjuydJ&HiVHOSRJ7@oJVGq#iyi}|?`z24=DBUFIeqO5A7&&ej z8)a;yTna;&O(n?DpO6+1w6xXcQpXU|f+anzoO z>>QI%y)p9&mQ3kFITv6fFyo%FW^6-@dDNPHB^9B|TjE_Rj1C??BEpk>SqeCpRhG@x zAiVj|7Os7awD1QeG?WQdFWFT!S?kV{ICRLA3vM;)f&etUzycuw#<{&RwLA#clca)@ zz9?-9-{Nb%;Sl_MV^lw|eHFD$c0C~(3nLPv@|h<~7Vm$yF1NNw(!aBr3VYFg_9jXV zVQXyWN?Tu!liy(UcnfE1)IJ>G{`>5wtVyh?lDGdSL)xdE($$6+9rKCXF87V37N`G8 z$*>9M>Ohfcvg2~PFB0GHnmb#?X`!~^yW0Tj6=*d|Mt+Rlk%OL~3tOVdb?YL=qA;{k z-H~STS}$MO0>&HX6y7$`!eIRULS)Xl)!%0f;-9|A$W7`Gghp9 z;+Y^Q+2LZjFBJWP?=(`hw4NHh^f$w@qR$ycxy zg)0elJD@++-S)$YaKSpp!6mubbPky;e9iZTke`?PL<@E6@sBXz9{s59S3vW`zc+>_h(&>K9mm}`z1B^W7}$+}(D>d!D1G^dV`2V(525x-3GM0e z>FnbaRsE#?cKdL`pVBc%mg~xYqyE49mcgs@4yq%n%cd{>+HIZ+2bZNWlD7YkVaWeF zNcq40*Q2^~o6HP7)&B%V@*&>pAZ_(3R}zguFqZl5%B|~tp(japVJm=}*YTjdQ!6pyM#Wd!-$zQ&g&JUXW*D$CAghAGHt#cTUF3ehZDPD1i zku~yw`w@Nl+!#v*u6JLas>cv3LHIFP_T=W+_U{Wbj;&zr*+Un|GZkrW>NAkHx<$Udpd#4h*8-j$VaCOK2J5vrZ2sly z&t4rs6ZZc!5g>E8xBYS&Yu^;bq0s}ScihR5;6WQ289x!FihPJTfKEEuj*^Oy{6P1* z&E$uWr@#9WI+yly-0?Bgdd9I~Fi@yhPZX!P(;FGCcQKZmVKGr4qJhRyz@@#E;+iNZ z#Vun=V$B5*vO@3NsyAWB5-IEY6xwL_w*`?ph%|=}*V}eSlE9o5_BRM)x=y-#>}tix zS-F~$W~$uUQ>>SgSMPeINo@EDDn9*K@{RlH??eyDyI``pFQ;A74&ad~ttjGt5gO`J zaBIeN_@*^!A)VO~F8v0^)rI-DYqtC-!*Jufk~lPP^^Cz*; zzdJ*X2MYUWWq%&831Vv7O}T{_E8nM(c5;i{I=BGpyinoSWdCVoEYHQt!D4;+-$Ds+ zAWQ2;j23*hILLE!{>6@wbUR>sLu|2S=XhjoZp%8qx7!$$@}DfV-?)`?-wpM4AKO>y zNaT_c<4#9UsQS+vyn}wj{j}wi&i7FsXinjdDG#^}$4r33g1!~WH@sT`wj%rre)VR(xOVN_JNcz|Dg50_uUSHb5=<{Y99|5QzDImM39~PJ znhxWBU;kixZ$(*uJUIyk*f%O+2Afo;5R~y_dORZ9vc^?PM0>PD=}=e7K~RKyF(dQ= z%FsMe9BwhK1&YY_xq{;);1Mi#?~ESI2;7yZ9aY{I1jPuT37zd??{u(uHagR`0{66V zzOjxTrkzO-xP0A}tv?boyi-{b#Qmgja!XD%6Xa>+?fZlXd4EyKW6{-;k%U%|2 zn4WmPOuO1AAT9N^yFP}u(ZcZZ!7;CA?wAUvv(c6c$E(I2EOPQ>OU(Ukn{Ns)jN=;} zU4Z|q{YE46|E|^OA5{cOeJfkDe{RuCz!n|Yo9nq$B=I@3Yy1~$`c+|M( zsun;XpDA)_V99|drI1Xglms{ZnG{NWwyB}XpN0a@)%2-{v}mfm$E%0XDS5g4Cpy`* zmQo@iYr=tfj69wAlFB@QX74C^mK5mz?oj#Gv+YT$pzDhVDqC?|>tlQof}He=*Iiwv z!BZ3$1HVt7N)8MRz|~FfCDCF zwnN16&4w*gH7_c4om6PKFToXzXs{eUW6$>2a{XMAmfPayZD8qQiJN5mj4=Bs(a4)7 zNas<{k6W6PTX;nxK^j`is+*r_Y!;W<0+=q_u_S=7XYzTtL33$_%KgB=kpYwZO_5cc zi9cm(lJ2)44#-YBlR8>_0>8dmbAEYQU$K9~laW3h0~O9_Q0-`_SOn@J{uS}nIX`)_!j?m_H94spyRs8qH!g&aQteT<)f05SPRqmx6X!PiMVBY?GD zp7$s!>f&hCj)8ZoGO;`t1%q8Fv%E1D;g9VCnaJf?Fkof~I~4pRv0LS&s?mCqq4e&d zZKvlX8o!-BoCf%K9!+Z1O%_cF40!NG6d*n(#UmzR+(d#0m?vFT?`|+pkJ;8fZVavy zZ76+Q&nxKaraSd0{S_|V?Wy+@hw$pzx!sT*zkldr9;K{^V*7@ zDK$ae1DRT|KxOif9dAA_@}{J-&Oa1l7e!8MytzaCdsOJw*HZDDYCuymX&(urR{KMf zv5<{`UsY^5?p*rVb?uR_VB}|REtPS_wK1RzvrIz9DoVLr4%ijAO*yqHkEtN>yN-UK z0HSPON(v=+>bb}5kUg?eIdRl%M~X44`zkQea8+P|Eeb(FxAwsJ4u6hTCgO6m%Ss^JqU$RbN*7SgEg- zo~?&Zr%fd&_`U!l!*zd6j)o!Td@|e#&Gz-miq>LxVfG?e5?7${$n>x^#tV20zfmml z!ufc;8A_4ik`=P)FmF~%;JPo4JZ6eMWY|~o8s^j@I5&NOJTI@383TU!8!b2FzUUtx zmYFo_l)Yz9N-4F*ducTuKGDsGju4`t@jqw_82gYF$8!x z>Ugd8POp9}P(7jBC;yoA2vxj#?2B$l`hIUy6qjU{9{IxErz82@@LS_|UcsDjq%0|t z<^v@pU}0NmZ%g(KucD;b77|hg95sLKc_#~=r##}d*|w8^U@})DRh&nzm#mg)mic^d z51l_=DKM{cu4#b8iXjiHK9Xm?5hB`8G=R_K`H`H|{#i5c+z{^FA4yKdMmw)jX9yq= zd5dtkyORp-=@K?)rdJe+2X$YuI*nh2Im9ieHNmqb|DCgT7@j5~UZg!xW%}VL{fezI zBEjtSPJ!ZbD8RjNpL1ihDSxoLg>AUoSQ_nGG^1IGLLA*oB&ov-?SQWZG48)!(x_J= zC_Ihd6!Qo?_X;*fG=TwMo!CEWF4)3{`_Ffy$Q~Aj(6VS%d-v=QL$8k% zV$0VCkfN;U=#v!UN* zJ&XM~py|LT!bxztZPItdA@5 z%ru*B*n<4dLXN558$(0?kq7)%?;Ff?2-ZX5!D!VKt`zJ}0aOuu@;X zybk7p{LHccv$?H))=6huC$Ez!TM?hXA4KimYn?3VoD?TQb+==2f4VRLBplhAp=zAg z3>?3!eCDKl%(!%m#Ad($inIJo8M7ln+=QI}2mtVaAM@<}OO=3EyESxzCunDI=2~C7 zymLUbGoBq3OPCtZu9RzezJH2q_sdP> zF?JmlLtgimekXKTL}QsOER1`plCO?>!-Z~cRPN-_-30;lXz{>HyR&aq=O}{)8zK2* z5VzeEV5wm|RUT6X@HEJRg2V(J?`-p>?9Fp9>iu<4SQFndE^@J>e0v%v2Z4G~1JtvV z&TP|M>0W(WW-FDpIJef~r8p}o1^DjGsMdzE$oAoohP|elS;`mEyWHcHy{ zR=(ihIt-6&W3rYX(wB@|1#|M!3v^LK6~a4rrhz6`Z*%Ub-=;^D$(lYv?{%`e(7LkX zhY_`oY+1CD#Zcro;#@GUv012^qb(j$hhN76IeH9tDu4aso3+jf^Feplx-=fWUpyP;<`MkI-ewnp(p{jUq9sFy|x z(D6E3_a|Tpn*e>lK+x)TrFL_kfe5PMZ%{r2`nlCv?5YSMaQ+Dh@T)an;AD;SBW?67 z;agCA7*$y|9Bn+GV&H%1v5{Z{?z8DQjR(YRA?B%(-SsP=Aev~aX@g&pv(bQEB5F7M z6Jo?6M7Q@e(XOiUdnqb~)qNUQvP#KHUR&J%+0e`yDkJ!yQQb(91tx0I9- zmicNtRFISG)8Oo-e z-nJP`28}un6*-EtIcZ7^@OGw)pYmXF zeDb&8c=Vb2XniXv9ow8p0^Jbk#%}fV+IJ7Zv#=qWNl!%VwxGoY30Wbn4Lu(m^ZUrR+kj2+ zIcW^8&p!Q|ywnR5KvTKx9ItG?#7EPhQJ14W$AH>NmJ|B+V0TXW@+Q8lQHMA4-1Reg zBr{`GK@qGR;I`db$i##Zw%Xi*yh1qrrD;(30a@HplJ>2iFHDEN@qp~LD6ARZ#Rp7e zlIVM8i{{SheE$s`t#0#Kg7tpqi-N)?Cz1ZfIqlz4>R$q_Pj<;B>NW#Tx-cL2IQAc#6d~ z83hh~LugAjO5ErkJ>0D1&$I<$1Djr@(Eq5e*L=o^dG9i?wy# z)sq?WR&;6nLO(34mX}F6%uIjtv zPn4!x$;u6RT0Ze!Z>^)eCK@Eg$-KgFishxpSf1Dzv+_bCbNVmLqr8f^->)<**elb$ zHB}FTjiLcg*)wp0xQi_3l>EE&my5aWV-gBV@3?w$9LYHX7!9Tt3?3VN!`ua0TkeSN z6w?*~$FI`=gSz*QYHDlOM}w$rbtAGBQ4moqs0gT3scr>Ps?vK91f=&42{uF!5RhIi z)DS`oEkIDDH>r^t>5x#QlMs?S7rMXiJ7@gvxOa?u|2T&+7y_=WHP>8ozVmsX=XvL% zw|u9OwS^~gRDDW~c7Db7&(FO7X!-@gnAoZ|S}G3=L9oxRXTPafv0?GamV9j0vT?if z=?2q;L}3q#;#Zvf8l#<}>Ja23m#R8~paHR-+!D^5AF9reFEelAm^Nf}-{14I+%!OKG<{?;K^&3=-{{@!D{{U8?TKj+X z*R&A?lwSWEC2Zd=7=)j5y;u5+PC;Ek-1aNKme!6m9;OQT5*)eJB!#o!E{$@0?? z)C@0@H7q@Hmk^8xpiR&eBIEGei)@g2AV+`8!Jq<|tjWEVoyYVpJrGW-f{@#y?3A;N z#0@@S*q|$YHibGB^B%DuX8uhNHVrYBml3EMV2S{hFh*j3#W@WOEVXk#si7>WFJvg` z^13%>IQ?T*X`omja(?y5ho^8ReZRRjXn_Y>@C1?x6O^8?| zA|%O>Ia+Q&VO``lq3WtwwKroV!V-m7$jLO*lO4RPcXp!g?)(Ys<(l1-u3t5|I3N!; z;U`=@8PPtWvfp=`<*5_G&wBlDzc8*p>UD&%s{Z6YV9o3hHSz&bqiHE-3cB-dX)p-B zHPy=PeGI79tECu-!gZPc#>&o#eiJWf`~;&<;~60(5szU}Y<$C#(t0?mla;x2Y=w)T zAC@(KLXC;yk?V3!5s~s_^hFfWA3V}j>V+^c3ZGb=;oTEOJnsiO95S@H&Sym^s4mLU zpAMvre5xuObV6VZZk$#sTMYYXwArBJClmq{y2<`85OY)2eL>|P!7_IqgAOsSJy<3yVFYt}4b=rr#cY|}X zv)kd9pIqQGe*zldw&Vx*7vhr+*zr|VZ7;)T2_w!QS8G`2^L6zC3?{066?inRm)IL* z!ektmH`du-HbZ0h+(s`_DD8Ywpy*6@*>CBHoVu#SwXU)RjavTw)1T5fg&(>_#NCYZ zYdij|K8cfepx`|fF%TMmmq<57)boR$P;{yi4c}Rd)L&~*L7~_j;{{=NJlQ>>BNDRA zgaA{we)KJ5rjTYjKup)0LctG@P@X<RNPHeektI4J_!^VV`>vxbtJ|iEh&iLSYX8=~Tav%UCl@GQ z{E9k8zb`(&9N`eokKg=QzM-Mlh*^}xecW@=wx;Rw_*pbPbWYpw!zYck5WO0R);Xs}nXpYWV>m1_-iIGym;j75V8)rq8qseEn4bqnSW! z(U*}2gwpoVK(mr)e6p-GwSGdr)2f_GYWk4r-NAo`-?5cNY2hdf7}>$WAZJVK^`6fv z-gHaEg1@wc2X7g-O&atcNd84r_|DL}q}N+%55RIVLb7AN#@N^#lsDl4Dej}QMSwg8 zN+-Y!eVd^gm#-!beyPyR(Y6v|zL`ORgZ*e$%rWsq5a4u6{cd{Ppv12(__UcY9et@X zoew0G^^D}eHsg(r-sIL%SBgFkB7U8Gt-vl!fAS1nUo~w(wc5ubzdbkR|mR;#jEySR`xv^MeBA?Bmz2e zU}<3smEDRd;L23pc5pOpfU9X`{+TTAn}tY#L$h8l;3mb(uu9>5;Oj)b%&YrUg}cdla64rV zT#4VD-(~|Q@y$aFt#z#bU!0g{^Wz+CP7uz*|*b!Pd>)ekdvD-TgRL4~z$SH)RFSf6J%7YtZBVH45MF1NVlrNCa% zfHLypEnI`5IQBQ^O41`5c7}G{yG0u>`T;Cg({%@AmuD0`F!d^;m(6ao_vjY?89F&& z$bYh7PkW){SeQ@hTEDp2%DlL#=lu=agtlL>&FN=#Vka*~bUhRV{r{z3f%`JMmsn?e zAv{lCZP>neyv>ts&j1j}{s9}id?Unm{Q;X)CvEO`HwRECG#J1h9Zs*Re*aN8ol85)gLXIl{eIhh2I2fX=s5mXa_D*2cq%BY4ZR3f+NNae%H)ux!2)4~fU`>L-d*m1mu7Er%w44g5+%)O_mOSsP zMT<4|Oo2EA2t`H06U`CC%2}4>u{5~p=1B@R*82NZJZ99-TKfveN%67a$h(MN)ZMMF zX9l3+6*^b>&pa)%sqiTA#YBkO-T{yNjlWI~cqk!kxJ*9iBoVDQk@7`XTk|HLf}qr&P2t0JK-#dQ*i)KRk3GnbOb~E*A)lQ0`YYq0fu|_A)W<|3BU`c@_VW6`QH$$3gOgWYt!EpW#Sf=s8`SZq%~ip{*)3>{QWd;p3-Z@psgmB(l(R1 zwS=27S}CWuN4Gc1Hm#XC&on}-gXsd|>h7c6kJx9HFL|>Adbof*%axF{x?AWxT4?8| z=vf_Y0xZM>;O8mcG-VJcaieEeE4#P0plBi4kxJ3w4I|hu9$AK1&0&l_~k3zSnPQ+GY zhDwjnCft38ip}r6aXGvVZuaMu+x&c-P!#DV4(H`gZuXNi;S9h{PvTywg z$q#sbTf!QRX3ygw>Ru4;>sqyrm*H!DW;Gy7sphS(SYkNKOE~=f7H*x&eX^NL_SSoG zykap3jv`n}b@|M&zH!qMQf3nsyjg|E1HY!>0-l|nF%iLFqhP-Y$q#DbKS42Dkr~NW zYrgy!y9>}sA>-z$kxH+Pm3@hGqBtbw8$KdVj-G+AAFs8I8DSPSK7seB=O|>zE=9cZ zL=*!b=oxVJ93#au_ztWcz1|*i-ES_PHfZ;b2zsp5~4KBWZhN_vAk*V^eLuQzvaoSHPJ(y}lKrSM z(#?wyjrYW@v#_YC=qgJ4O5dr(DG8Uo9#0QMET`v{wPyv+)&W`*joJQ}z7B#~Gq z1ojdJLf4dD-IfYLZKr5+M-NiKQE=OA@DT6r_uOYz=$&)CIT{y%Oy63aw>+aktIt@p z?pE}KTmXW2Nx@Rkk>#tj_i_#nNLh&+QGooMLVd41>1CN(1^qrXP;Y`*RJ5q&K^4IZL=-{>By&U{;Vb~A?dI2Z-DOZXzcp= z+bw#M`n$n-K0e!3RQn>&xuuZcVAe9kZBH@KGz9cf8qNtRwP?K2SU4LN*D(EBC!-_x z5}XNGm9Wt8OtuFsmT~iH=sA=yiym|%p?Ek;>)(P9Cx<9BAMyMOWQ8H!dj zeY(LYci)es)PZC2#=bg`FSW1JVaQhnsD5A_fTkQ``6c6s%4xr=;r8F(#;o6Xdv`$b zIjz*8+EQH(Xd&{;+|p)=mYhBac9)qV`S9RtKB#5d5K#!PkOC@|;njC3JFBT~=f)e= zR$GaUTL6B#lj7W~`#b&glDRpBO-xB(fo(BQS6}m4{_AB{^hkld!8-O|dB$w=f+_H^ z4MgD?h6)Na%HU?~irlrZqR_*u0ywLImozCpyl7|Go5!I}JM!mjSEN06ZVtc$R@X=~ zwyVX@9DDr8k%q;Ap0nUMPW+->@05_5r9RQAk~kvFL&p&5vn(TyEwmY zajQ)GTh*b{vh(1Z6p#z7txr|{`ANsK>k-1TPYCxGuClP==lJ+MH*yt^p_?W_YoftN z{vWZ5sl-;N1l!%EL?V^PAr(HLS1^F=+12Af_qQwUoMH!Q)rB%!I#Vrbq*i9{B!u?y zCNk{!<;a;_4Ei+Dm6rMSC4W3R^*h|FcpMCZ51RdcwW2LTm2H?j(Az73bff{}O=X-3B12)9w;%iB)7bu3crDpC-%&w2U;CPy!9U!L4V93#-@NDzBWK&C&anw&l+ zaiL!v_v9^c(vjlvb_Bn@k!6i|a)TbC;UxIclY+H<0NN21Q~k702+yE)^iM2RcgmQO z4j3sl)<~ujzmC{hA-rgzvuE*q6pu(a^GZ^e)(#*3rtisDE6jl_r0u<6hh|v=(<<#N zF*k>bAwl>DRxnnwALO zkqVhSAJNo?t1qso`=Q8q2kntUil z)4=Z8)z;(IjJY(^95tSM|9R4@m%w_~d~wn5Olwiz++NQK%ecq&PS@<9d8pfez!?XO zcdQ;@4-^cjlS+u7K}*$bL<5`+N}cC=h3BAXmwU8k&re9x0N+EV)Ul*i-v!VLsPkmtC73YBkkr@g9Jxt${J{{eW*H7qMbVJvCZi`tkYEv&hMCqHm z2&Neilq4sDx_cl@;6Z5+*wFHKZ~QL><+aZsXQsO?L`*S!Kw5jq3di#F-*EXigZB$u zxLEqb7C{qRT4b7hhN?A{p+)d5M<3kmS;UeaOp2V{;f`0k{#D{*;t!M-g(r6DaU{$=D8vU#HJS$!FyB82OJYOcU4fB}yB>v)ZwI@x6}-Yta@>q&xP>Z4*> zjqg&*!yN6$-(R>XfynerDpx#Ywa-l9Yix`F#N(p?MmB)_>;E~(7 zzt(j?%nW-kYE=4#^vwbNU=NRU;SSbdbn4P`v&#CXmHH=g@0@}?K3F@bW z4lD*vz#&ooEbPdk_duC#f^H0=iUrMq(N*H=lWcz624sD`+s--Q;Rs}157dE4LW0$V zFdu0}nv9V@2}#OAb@R1%mV7nD@Vs!G&}mY@R1 zr}-B&PiF8sC9irt{vHoqe`A-!l-eykOYlob<^)NQ1<;+#{aZUAGL%|vz?@}pPQ|lb z%+{2{DP0iPam?)$$Z~dTYF>@5pQd2D>}d=Blw%W^Lja$rBA4$W#hsaM(4o>n>~`U9 z(sW{rBd}t8Qc@T^W(Ue8`;UcktqlPcO+GX{HZVSZxT+!CI&YPO+s6ebj|Cf8|ARFyT-5RKojW#bJdU_-`ZwEe3!g zk7fXX+Q(mzdei+5%ab7OU0LumDy0+^z6hlzXrf z0OI33nC%Yn#B-geBH`TiJDXYz=gzj!26$r#MJLHmZKa2XnxRGJ4^aU*4aD>fOCy$; zUljeJZ&gPq-;ZY1Lj|^R@a5*KK;h*UMkA)bYw*4Xbh+NnPN)2G+}Mq`)PDf?JYg*D zA1}cF34EWTHw}%y(+D^V6)HhxCA|h5Qv$H5xJ+^U!YgECSjw_dj0`@0#A}tdvbkM7 z0&2o-1{d%Tqv5;~9%q_`Q>YhGpypZrIK3xIK4#v@u`0t5Rs-Ci%XJxg^^mP|XSfty zrVWLs-F(};uUlo@NCnCN|BSs`vaD7_e3HmL!lxtQAJkV`n=z&zX`VIy$_k;NIoIi$ zyA3eGtgnu8%s|OgTqR3S)DRL7|#%fWz!C0hk*>sh5;Bj)2+r-(b)d z@!`S2)vxeIE!z+Tm1@Jx??TXET6Z2OQN_)@A_iwSJ8h4_9~N3ZBu;k#NWn2W5Ec={w(gc$gJL-CooaFXT@5y&Xc>GQenY-d zg>{rjS-C9O_I>rJYOpJ3W2NDCv%T#;y0>&Fz;RmGc}tyUyTutCw(fygoy1Y-$bgRo*$;SKKa^0AgDHFp%*9lx_^IZ3erSx;*K0fbc}$s54I0 z(2}#B=K6A;??W#I+bYC2PGfjmv@UbDV`#pB8phi)qxyH#1Ng!+6e#W@?t2VGAWfzu zQ1K>`bPriNUoz4yWs=Z4zl>?4C`zn{E)%qs*4+dr*kHhVQlA4p2^)N|=oa~MKF2yVRRSnjq=yCxI3@(7k$^V}bIaF_(MsM$#05jt? zt99zbNVqjcjj`zj+)G(VkSF@3AzY@r=itUAng}&}PzhyE#Un*JwtKvEHi=Lys zz7%5OFps6*ACT>slJT2*sV21^taa2qBYMTOC2t5Alf8_E;~bio%HPqTl2OY#ftr`k z(O|dK{e3~ffbD2yn94mBh<02R5NCiqe<@)rbpPgYKd=MJzlV_7-#)<5D(*sL28*2< ze*v(eo)_`E#spx(rj;^6 zrj=C-8N4+Wc?2kH)k=8`KCO(f0q=1sGRZd-<=rha7qAQ39minN%mQ&;GsXP_gGHct zFd)BABEF(1AauY60C}kq@L|;M&IcmAls<6=*mV)u@&92J{^tVnKu5@-urs;H-|bV_2aY@*wXdt{ zl6V0G#f^0Hv$x;eRkg#0X{+@vKKZbRf%Mztw&?RAhSJL{qgpi5Cn5Xw z*Z8^zmo|o{_Z>T%c~-MffLv9O$sh&74ir{C2*QqJ#q&9gt?y5EAq_R_q3-F&q*Qm*^bcIO^p zgwKXKMi@Wi1KL`tL)gn{11q+vqkb>-)Jev9qUKMWHFL0GeVHBB^ZFn-#2uA?Q1Hsj zK#^j%5ws!6xgm`+gjXomK5AprkaJybM;HubC#%ZLp&uO3S z^0H;*Z*_IbKiSOjc-c*LFp?%#i;GC>d8>L1=n683U6V6Ku$z7?&!i8;4b$B9buj4kAHPmePm2kBEWy zpnpFpmZ#+@<-zq~kw^Myl6am(2I|>Ux@_~;UhfW361XvP-pfHQs7*|;Z$c&mOk0a~ zuf_>8qu~<)(O?u*IcYlgy+~7|i>4Y&oI-6lyyhpcmTkLNMydBwW7IuF!YZ$m2$lA% zeHbwkfomOXcV6M429uO@6+3yVA~6p!9aiqKa=XjVYn}Jf3uGFPY@)yCTDI`FQ2nb4 zYUJ!#tj?i_eHi_WKt!QUFG<2F9jT#dlX4Aek(iM5Vd%6huv%9fZiN3q#OFwqBK%nz zXo*wmsmn6z8SvO8X;%davwZ_nqf$5j0`0q8703FJ&1uwFQe6;b^vA=QG~T78H@M6O zH`Y;~VC%Jjvf`Sb9w_W_OJbHAsqBw+6ueZ|COsDsreddCnVLoI!)P8sgE4URRe0*2WLCyO zlWWm4oQIp_!h`W!L-?@m@KenP7$K}Yr_}r3@V*95)!3!ie^ZkdzxSeXjn5pjRkeZ<4r+g|Rgvh-#C zPS7K^oKuRuJ+HZws_E!En3JI%4}MLT-_MQQBnHDLOQizB z^JjhBA@~d43V#{7@7`w|;Wter)wlG9{DY~Tif2g+0$DS6dj0u-fFCA$3%=#X79GJZ zIY;f!Vn1{?_a{aU+y6o4r>~M}lvK#?`GbB4yl#UT6MI5MX`yWm#|G3msHYKU`1pcG zZw|T4jNUIgYio$AOOoK{wU>F|o_qu=o|3JCO2uB&RHNtsyxc)5WZvF^s_8VauF-YLQ@9FOnx+HY}E)RoWL27vfbEjFP9?8DL9 zc$3-mypqEgV*cAnFP4|@%-?CQn9=xJzbPLgzw`0)Rv%pU#OCSB(gG_J1xlqI9b0ve z>jPprtn&~irtew^`H})!m7mnzs-98f)c$eR5O!{2(zISl^ z;`CbK)XfT^q9;5mw|l|-kh-igjM#WTNpPaYeK>>7J}Sm1*G72AZ)L;Nsut#)QLsP*0dD;(a>b4ePkBR(kTY3MFsVnHL z6B+9zLY6wA{g_YZbap~?a^dxbaR*k>g1Fi(JGFD`TGBAs;-}Jpfjo(l5LMVEngG9o ziErPY+9Myt^V}^G{n$)Ur)xjiFKT^r-dQ;tOKnHt5YaQOl^*ncRtu;)s7~pzeeReBvz7n6$bOtRAS+bc_uJ@;eM2Yg4S4? z4Jt;ke}|JIl?&e?C8+acYU}Bs5buLR4f7BWr;2$5>s)5PGMh32MVMLkK+!RE!0$BHMAf<}3Wxsp|PwSg&l;N13AYh(#5`Mm@e3t5ug zF(W4%9tFI?XFR+d2q4#w!}kf=k8P5{Aj<6%P3cbyXwPq{x9ts+`dA> zNhI3l6T)^sShnV7#bA#rN{TZ1&^?qQ(cZ%AQXk`Dm&+uRl7QX0fUmbFAY(OGYQzqm zQ-;vCqG2-=7Vnz`Ldb-NN%fxyWfz8ofy2}J|6uzE1nkZxaM=G3Ur*AEaT>7n#P`qZ z?o++j(Ch>OaMmZ!xNXxX{#;wS&&Mb3pw9~s(n|t=W`jxb6M^@l;7vqf!&NYzv#dK; zd={zeTh%OjAlZIy-BIp{y%pG31u(|z2VB58d)Lu8FkF*subkJ8?;D4Jl6lE7or%5r z=;m;>-P5QIm%w-~OS5_p4eGco4x)=JUId1*n_(}a({?{E?x-6Wu5)>BazaX|JbgrI zd>CuACvN|{AmpCBh65R(0K#!S#xo6;ckB+!tLU|?k(naRPd!@JRx=03h&emBaV2?1 z`bbv4j&hbxghPO6DcVHGGr7>nq%>>h&ey;I+b3aK9;qE1OTPYf&Qi7R1>OuFP3-M~ICQ2_u6OhA$A9!J0K5LTeDbg-N9-_cmG z$bwu(xSq}P2zz~=!kQyy3%@{^Q>%|>^y{2u}a8g=e~TtohqeFM8^9yfKGJ5Dwuw*;^EVaI*Eoh zZh;vhlCIbXLcZI0(b0biEnuRfs#i7k64DXyZi12fOA%D z)@*x&Wu4}?*#Mn*sfH80;m@yT!+R_qEP{>JX;r)czeNx%;)HVM`1b0zw}vJt_be0WZzvByLsx9ISqoFPOK@`A}R( z%&4_9{IuGlwCnje`qGUQPrV~%Vp)Uns=S1QnI0LS9x$a%O_{BAf8fKv_CSw^m$7&b z%v3=pGu8n$fgQ1`S}w-#r3?~fqjX#7B>R$nvy15%2^S9 zSf~)inhGUV2LH_7?l9##eA6p^n}4TQoy{Jv!9AP{d7k^B@!e%s!z+n0yw`o?9MyRR zXb*-Ucty-R`I@-38qqqs?ffI=OQ%Na7H+HgC{l0Mtlg7t`8>~5(@ypHOXL7SMJjOL z?$9N*aC1l#N-uKLGdx;rJVE~TrO>ajBimtLhID^s&lLeQIukL~(V@z3^DU}%s)ZCC zBxvVY#S<}7egf>aJiLYevTbWA@p{{_$(B2Qp;u8 zuS)~BxO|OudO_iy`s0TT`9#{i06_RqAcbE4dLz}TEITV-X9}m&^G#!m5a|Dk1mcCl zHSo?e_uPC|*tuf21${c@+h^kaFPR)_ew)}k%VamF#^oByOyl;&2_cCR#iAsEPj*=W zl0TI}=}`RK!AgEUDt*&Q@efppD0j)bMVM+~5ZUmCY;_!Sp0BM$rJwa*#l(o<;57R! zC&hIUK^_p@_nYv*!a-CY@4$iwc=W7UKoQt>{hv%suhQ>{u!@AI=^)#V)H#>wP?{Gq@8s^2z*WNAXav5n*YPIS-F#Bv~@%hBPYm#6X&n+r+{>Bp)v=V+@}C zNI~wgP3Ec-6mdlT?=v6g(OU(8w)=sSW$B@UC{vW(ISbdqK}EKHP^mL3WWuTXm@`*H z8m;V=UrnL*u!qij4xvW37bid{SX~<71}i}Y6qES)7Ab=hFD*b`#*-GXQbX{6%Tu=lPXA|4x7gbp*O%XMITlP5 zeQ%jWACoPJi*gUiKLoRgOtE9D8jV4Ik``iReE5=|8(}PWI)F|7!I~+?&e7|e&lj+m z!P2AMO&&a~baQwtj`DULasE0(WW9a1%QnN>$gB+^EbCGiNVdZm&jGw|QOm>)8 zfzslRuX(u3{$PMl1#-Mcc`|h@n)-qCH$<91q6_F=rlD+C{K4g4Iu|I3;VcBfakuzM z;PC+hh_?UuM4=b_{HAIL9sxdck5>fKuiXy-dFs*!0JkNwA{0UHzaZI{FS6D#Fg&}M z(hR;|3*7=|Nsk6B$rhA@3mMm(_^5U0QR}(5h(?wKiz4ZPjY{ACLg=Y~ndmCJ>pGWt zrdR3bZYMs^KLFdNPx^Invfo)cb;)-pvbAxB*tulxmn&#ZFd!XZoW{8n{wx=5Dhh=f zTiepb!So|et5Lu+6U_7$+VcgYiMq9VuYN(|YmanauL3nNs0MfV!F%ce|6w2{2djc2 zIfl3;yj(9KC+s^SR60dsWz3r$0tbAPt~BTT0cC>mqpLlY5xl1=2BYl;7QUBf%>5Kz zpcNlJvwP9*WghUVSEB#g*!Myg6`E}!vxxNI#s}X_TE96}c=wQ=<)@tk33HDd8w4%K z`oDc1n}qUc_xbiYi>}Q%;}_RZ7qtSRQRl0q=MK{?o^BX=+4#{ZY*H1Dj`#t_HZ0xC z-5sWn-UgfafQ;!-1PW~p^%1N(UqGBG=K?FEdlA$>K!3S9`yC{mn`vV6&~NE-Jlrr{ zEkT-!G$W7s59m*aq+D5R4M&jJcsg{Ou=>h;=4%V!04AsffkM&eUpqsW{Ga<3sQ%7w z6Rg8T(39r_WT^eYiZ7yR6T9`i!pvTHRxG%n-lMA;O(Q>m*yJ= z+S0^2)-gT}#FPoC10dBQg}@3D=kcjpCRpxra4r5hjUn}Lj>|k+(9-T;r^iuU=CE+( z>}?)@p(HI|lcQ!`LZK!M{|4}KP<5}e?Y+mN5l`j37Ieq3;9$j5r_>io@^Af?a1s2R zLddRuw|QN_AvRSoUDlPlfa>4k4zN3KeNeBEw`R-Le!$y{mc>9+J_vOqI4T;}bsK65 z8P(M{z>PFY>L@be9qUeWeU)^|Veq~`(9*-TYoTtFPI5)?bofp^VIqth-`7w~w#P~x zd4cS+bg~i%!-E-KG!g7Q(cn~%!f*MG`50!ci6qgTsFgy!t{Gx^9y=c^q1%*tkQ8l7 z0BM75xI6)GoJH#h@{RYqO3M9zKUhWLzuVn;q6_O6Wdv?ZyPA+vRGLDZyWt2{wC-M}D8= z3OrrHK_9-B>>874k%2`WKT|~w#Wc3DuB+-Keo54k+E5+KbpX^KXCeQ6dA!ZeG@SEH zZ{0fPI)qxjuZ6C&yZCmI;oo0l?_tUCxf7}tbe)Z<-sQ=BI&%fcONIfKIBx{?z?XF} z>|?c*JYc$shxHdiS9Y=aWbV)OXZ4Qe`~K+DN%~?)m6R4z8#BCiT`V;TWN8r6|IyTU ze*2^v8KTI)ELT22aRV~Ax00R_q4qJ~p^$8Xd4({@AmHaEQJ#YCJ4O!%1Go~%CZ$$$ zSeMx!&U{Dq0v_328y0`1`m8h-sW;ZT3MlmCLA*e>+28S4Gb#rD8g$eJW#(RG3CBIX zq#`EW4Xm~leYmzyn42zJ;aL6v57@BRu=mD>81TdW3^F_xhAOfTH1uV?%KD(k&KiR{ z_QSf-$R;?(lo21dKYM^Y6ip5i2nqy=`LNU#*j3g7Fh0(?ttQE79Dq-ol+zTUe&*cs z)R0I23%*5}`FUSw)kE#F?&X=Fd}A)YVgH@EV1L#ghTzV8{z>`nShxAdU^pm)+q54qNrp>&5Gw?3 z?<^(jG#-d6M^8C1fB^-v zS5e{HdlFd-f33JRwz2{*kDuRZ?ark^(KF}xdNGHw4a7ul zC#Bghxw)cy-$aqkxRP_>9{EVL%MRGIFWP-rsP>S=Bg)|yH8jrYgYfjrdb)W3g%+us z7dvrd=(uG<2}D*sud}#s(>_cSPjA^eYTPFdh9LL6H2jzuNB;BZnLdnZK2pu?*9q(6 zzJ0-eu-@8Z!prjqT7=H!wLS1EO1Ng(2f$zJ`ML5Ly9nVbuPOIrDM}HwuX)QZl?*4p z!8cUUfi&`3%J%5nyrE9HceHIgyJ#fR!NC=*`Qw|)AmEMNby%T&iQq0*1k_IwSQ)=e zfYg!`p%3-|6E$WeTF|f^U?K&o_qb>U7H4kKM5zsoGhft{>O!QCsKUv3mDeB=lO#Ob6;|S^E8DFUj@X~1ax>7SOBlJa?UnXJT;PPfLlOW4Rk4Ep*S!x z4NDRFw$78eT>YQ7?2^gE?(1}}Xj2CLCkunirE_~h4j}zYvrVvqpZll@3<-0b@Hez8 zGa=sB>BnAuY~Mi!-~2^GwGk%LPknPiTzv&ytYO{h^}EGXd#og?wMZ!Y+2_ znId!0Y2d{5Gmqphx0Ef6xf`f+k=s;9=1lR)I&efQ0x|MwdPZv}*LBB;FW-r^)s%6N z2)~ik34oWhhcS!U#-TL;qVBgLB*{F4nlF`(42|DG z{8{ZL&dGlLgvFsWYoZ0x%z1+s0Do08h+GNPzW7K z?aRvB7Uum6QiA}V&a4#6J^%RfL5)>Hqj-uzBe)eq00wj5DaXkOb$R?tGY$|`4V3l; zX28>XxJUBl<7PdREhK!PCIkn+KS+L&1Fz^_)+BNKe}9c4I72Nf=|4mk@^Px43~jcV@9kl zc6nyiD1i*OQn%%YHf5mYHK< z8MCL?ISgg=kqK;6RjACQ>x=&A(r_GviAZ;c6wfpz2~8ZeH@9c3Jt-P+Ca5vKH`wVM z6LN2X4>sS0G$?}wA;6?^1)i<;x2>A576EV|woN?I3umN&$mY_&mryx<66wx|Wb6T= zB@cGLS#_p-<>?+?6N!54{kisnIg{c0U*h^dF~xS;xV@2=io_$CVCjEnn(YxtusiiX z7sdU*YT37&t^l0XLws8|goxc~6i+@e=w+I-COka*A)@kMgzd%525D-tbQl+)abRnu zm=XwP?brKHZ5h1phh4E3?6d*^wTCCIViMfVHy9rS>1$ZV<-(5&BrMaz|Wmw2Rm z`d8o@(ZK2y*a^Pm?v`SoGLk^cYXWT+jF=l-eshH-X-2te^n$MHlbY>Bo`8 z(yZ*=%VbD>*e18;OuRgDWh9=DTXkKQb_-PeR=brt*8qZ0-PD(q6C7N=)D#)M(d{8Z6%A_Q=Lf~R^=OtHT{p)!;OOY@_H z?a>n>x5DHZC*#)64OS4uSRKzE#LXZO?1(@Lshi>|bsg%mx3r=nQSUGc!g zywC$&0FVn0p%c-MfZDiwySg>Fp>^Jl{dyI~j>~7R@X|(h#c~L4m=Zsij&a>};pv0M zo!OL~3QS+a84GeeP*Bi3Nn0&3f|RL8R9j&d{r28#D;{nA(2=m%UMOdrL^Hr^RX^GO zAqGgP8_>7Ab2;fc)Ez~*;f<(_>_@_c-4rqIsvXM@a}&FdIIrXTG{Iz9d+Y@l+#S-l z&czCzvK1}o@}05ob5oHfgqu|MYK`@4_7&)np+WgAG#1V54D!=Li-}l82~;)*ZCz@2 zNTff68{{({2ssr_DC54{&SAyoV=IHIAtL^N*;l&VNVoS$}2n;QP-3-=;1e)Lc#E7{^-ijS|@-rk3h9cL{UHx#f3`zgiVf--Z5 z1<8MaspG@6G(YEUh;Z2gFQxlrex_VAOVo9pX~x6 z0PyW^M8L_?$pIO_G|~zp*$i{iUjvRp#~)|i?*w=A1+TU3RUd(%|Lv#Ycv9yOpj;7a zt89?&liez+vddTHiSnF;W&W+pWu9^N2gp6@Z@q2l{t37SKoKh1UD@wy-wu30hx}Lx+n2oT`Ew z8Vy)@u$*LJJ?O5x5FMVDr?XuOHr0yT4(Eq5UByAMtvO>_J(2Ek3W(TQIEHE(&>#b2 zGCn$W7x#!3^B4iX`a4H?5CAdv4StBJOedrUNa8>wI^qkcTFqmg>zZ4f7gFBlbpKoeUJ^^Zl}|_h>P}r_8DGER zqm~`0qaOZ&B=&lDEoby(y$l35_U)z=^`Z&C(N57AoYamw2j(qEUL*kzmu06bB}fJ5 zf&Ks@6CfSG;Hyg1p*06dsbzjSw=s8&ru=>dEHs~%dMJyvrfG73w}z!;pL6Oje$=0& z=ZA|%pPrlk6KIZ5n(=i5ZP~cy+oW^(Ojz^kOT%^FTZuY0RQHTCqiZ(cgR_Tu$__c( zJXi%6{71+FW+*4XaAn(XNr1k_1x&5^7sRy4k^9@DfTbJ^F;TyX%LP{wr#tTAVCVs^@03-ybsd) zn_0Noj`1K#ldTnyh$Jk#uC2hf&}%4_-%Th!Ya=@bxAFD`3&SV9XWBhkU@=L@d3+7? z%TO0LvF`&6p0>f!>$r1vAZJieIClFB6x^%Iy6@E5@#C4B_ZAj zHs$aXU>I7T!hJUlxu@$Js6*N)X1l=7o~E&5&^{q|%G+|xMXunaKVMXd78y)N*MT$6 z7jnh(i@Nih*jP+2lb)zpB27wHg?+EmLjAXDz!P5P3h^n!B!_p6NJlI4ysPxELvhe8 zz4V~ru3NG`U8T#PBvk5JmKJd7<|gfR`F}1z6Qg$sEp(b?PnL-9qsnP>o~R6E377$3 zPlJO`1lT>UC*;OvB+Bu~RwBmDi?mkGxe0QgI<3-8joBYq{g({jJqt<(T}2W+ivoVR zaP}K0zE{22Bz3(?Xmgz2eJC#SB=%f@MOi6tS;<2K|p_WtLf@iW~9oE#4SyuOTx#M z--V|&d1>6eFWa3GlZEsP=ptt%q5oizdYZUG!1bzgfikCcRxLBs0f@Fmnz-**Y%bCS zQW_G24rh!Q2sF!L=qiizs}KKpNLru^#PrV>f~y1vchMLdw~xf9fLM{X`8`9W;`3Ag z>eaMP$Y3JI-QW0Cd)cMy9GO3Jj97|AL9+T8ppA1tmcZaU%_ci6u5&Zjl=swxNDFRe z*YD}gyIY4hot1O=1W6uC`Ws2leCzVMJqAK5zf%uyw&j0R^PuY-KVI z8gZT%gr|9(fXcHGo^ts`U=AdfL*&n5W>vg>wtVFmyQCmJ1GqYmMiCR~74W(V(>MH9 zQ?dcwAZzkY;3lm#Hw$syr)IX;$PmgQUmpSRL^m9Bm4M9dx<>xmL?5=05ISB7=aUc1 zS?Z|>G2szt*K{wA*}peK>$+Z)bl(m^V9{qC9%9?yvNmTwLxhrA;l14hIn|L%waRyD zL$1t!xeJMEew)>A z{jE2)EgPX!5TF_DH8&*&%%NX(2Sm9j5C*cTCrw@>^}zGM7{D?x4Ogu)`9$P}|4FL;nJ{r`i_h#w=JF*Hvwjxpb9} zTTwTso706C_p|=cO}tqC7ddMiy8|IWzb0By&wm!0PGE9oPv(f<%tnA*1uLU?#;pkIcbc&uH)#|ut=D_s^875o1 zD-P)^^7Oe#?{!YNuoTd$iry1}roZW4&7EWN_)D*S*(Bss1nSE~9e8~Xf?@wQkrk$= z?2VmVZ{43w342o1>iih}4h+LvnDPmpKy)6QpkiV#s_~>R5hzYy!^4bv3lG?A(IVo6 zpB+7>pBRR(4ox9eh~`C1DnY})y>%G|(_z($yBh--SKSB6|c ziti9jEq1TBQU19w=W~wvmk6Ljg5zva=sVqxO$d!n`H=qOxp62oMvHC7^zzda$0~S8CbXz zpaG220K!!E^TW5yng{$J*?`8I41SIc(vzoBSb)hW{~gGgccE!d4{#lHT!4MD-X#dq zJkwN!uX%XP#Um<2U+A?r=Sx$Ho6{OVu(Rl^5ri<>M(PQ$kEeuI;M{?M@RGHP_G61O zT6%Heb8+nnV4=@QV?g%rT{aN14%Dt~1WWcT_K>{?$dfn>PYC?~YVF*k*-Y~|Zf6{3 zYI-p(+3G4~woJ2xHG0!FZ83u?RU`x<)2a%i?RpWnM90ylrCMX!OlhRkHZ(4YYSjo5 zb!}Z2=G|icsQkknw8eQIrc7kC*BXb&I@n&})5yRJLw(S*X9QiA+W?ml5AcLgp0tN{ zbk@)o=u#UUY4yQBVD192dvqK|w7R^JM?>+Fj+P0%67h@P>PSkA{L#BYFNxCG=}=|V z{^3IhPE$a0g>2q;^u4xmcx%DwzJdIW z?#MA&Q>DhTx~_Zkt(i>69r$qN_@6MhVoK;^2G%*`YvVs)Rnfmv;cN$DGeIqSwFtg& z^uulOB~SyPV^@K%V!GKYx~e-E-(|20N6jM;l-E41XU-0m3ynPit1N}47|y9*=R z;OAbWQ`9wydLtkcIUJl=eepFMXHPwcZ(iJJwFs&p;G?c$1b+ivfCxNfV7U5#B4qUP zgHaD=(g6rrO|2s8w+|sQWe%?Se&ZD58IvP9~eGvSJ|vg`6T$=ZoHZ z|C4+SK|N|_qGV$s!<0^+Tt%!mT^KG-wZZ@b=7A+phQ=(QtUU+wEW0r!^MI`|0EY(( zGr@8g8KlSEe|BQFQr@kWzb# z9Yi*3Jn}afBPo4H1q$ByLPEaqkx{eTA*DfuW9!uFMQr zj=AUE%oPJ8bjGGC-1%ojJ3lA|E_Fhcb;(1e?{mLXImTDX{lB<@CysgJ%*`NpbqFVp z4-3l&!Vzv~SfXG{@+j@pBr)A+7w&9R1TU@>qSkKpMUDKvr01x!az^X;b&WL_SD&<2 z7X}_g#&7v4PK#O{fgi~y!J!JU8znCpade>1IRDyg9rP(xsMD?%wsvwU6aY;|-L0v|;%T)*}fcuv=YQX`5Nr zdn=)kjvxmd?3j=-2h_}$m4Vh3vzotVRB41f!%WWhka4MJ^Er$T&1+XRAO22zkCs?& zB`zt$s&G(_fP@OZ7Cm>jqo`%1BXBs)5nH5@>MPTXG#Qxw*XfEa@4~V*0V-wSD@?z@ z{`-Zb8F08_p<(vgiffv55t7}!l1nfGK-U~F2Dfd)N){ctQ64tzM~PlY0`U6X$HAeY z5#p1182S0_uyueW8zryi6=>_=H5bNf9s;fKTDMaoG605)eR1$=I8*A6g4w7*)ue}U zem|H%l4J_$mK%bg%$(*mjwx1dZk2+5VKp;jdF*{_LLES>J?AvBUN2cxu6Svy#htf5 zJG6Yi1bdEaJ5b4+DX!E4<%f zfw-(%IO)IDc4qW1@002M;1^@s6or`?000001b5ch_0Itp) z=>Px%oJmAMRCt{2+c9obF%ZUaLqS79g`f!yH$ZfoiX(8193pTb4nRRemI|Rbpn7NBtU#W8KR#A3A!~>PRQs~|qh7*J&)4e*mhsY)z^pK&XT3Fbd@sDV z0x+UZ0CoI*a0C}n_n4);_M+YEB>{S@Kqyc;PEaSRI=;2shO4q6EeEP;+$r`XH&Cs$ z#(f8>)|VkFZlcu>P+9@KB|z&0QOB>L>Luju#n&ctr_zCHtN32!E*)G-1~GT!j!=Pc zfLzblx^Gp-9|3c~1LPjTd3{7Tj9k;-`cj~d?+q1q;Ya{T_3-|kH^cOR9tP%tH;|!3 z1%wn82aCoA000000002c%E#^H&iUi%)t0&B5|983HrS=&p-oH__?`D6N3r5}gA^%C;w z)zc<(r_zCHtN32!E*)G-1~GT!j!=Qrx*o&idcImc;oK7iD$wD&PC&KXBRH>*=!TJN z`deQL)bYJp+87845&%*?ynpA-Fg>7$fqCE!WGGPq?{An2=s^!}AOp>CPx%D@jB_RCt{2-M?J_tKpQV#<=aeSs=d$4sqM zR(Bubt96#&|NiM?tTzJUT4(t! zRn6>0)#t9|T-cfpW83v_&Bc>h%~tEIjhW%f1bj3aC7Gma@$GpxmWTK6ObpgT7j3j+ z_*2gf8fTgyW%!G;L_a=lG(iu&s``dJ{PXE}KR^9sd;e~6e{$m3TXGowc-avYzY&_y zwPTQ5P`Lq$n*9F&bW2yyLe*8T0nAH37+>DLsr2Jr)z`PhF~Ef!1Kb8@fogCTXy9B2 zsQ!H4>(e4vg8!}KN-#;ZeHNtc`fb-?!2C8Sw8Y;700000(0_jb#kuRg+pS!k00000 LNkvXXu0mjfP+TuB literal 0 HcmV?d00001 diff --git a/assets/art/effects/effect_sheet.png.import b/assets/art/effects/effect_sheet.png.import new file mode 100644 index 0000000..16fdc40 --- /dev/null +++ b/assets/art/effects/effect_sheet.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dgpl0g56pw1qu" +path="res://.godot/imported/effect_sheet.png-cf8021daa1eb789ae18fad86613dfb66.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/effects/effect_sheet.png" +dest_files=["res://.godot/imported/effect_sheet.png-cf8021daa1eb789ae18fad86613dfb66.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/project.godot b/project.godot index 4e5c034..84931c9 100644 --- a/project.godot +++ b/project.godot @@ -32,6 +32,31 @@ jump={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":32,"physical_keycode":32,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } +player_w={ +"deadzone": 0.2, +"events": Array[InputEvent]([Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +]) +} +player_a={ +"deadzone": 0.2, +"events": Array[InputEvent]([Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +]) +} +player_s={ +"deadzone": 0.2, +"events": Array[InputEvent]([Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +]) +} +player_space={ +"deadzone": 0.2, +"events": Array[InputEvent]([Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +]) +} +player_d={ +"deadzone": 0.2, +"events": Array[InputEvent]([Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +]) +} [physics] diff --git a/scenes/characters/character.gd b/scenes/characters/character.gd index 9f434f9..713ec9d 100644 --- a/scenes/characters/character.gd +++ b/scenes/characters/character.gd @@ -8,18 +8,22 @@ const GRAVITY := 1200.0 @export var attack_duration := 0.4 @export var attack_lunge_duration := 0.18 @export var attack_lunge_speed := 220.0 +@export var air_attack_duration := 0.45 +@export var air_attack_lunge_duration := 0.22 +@export var air_attack_lunge_speed := 260.0 @onready var animation_player: AnimationPlayer = $AnimationPlayer @onready var character_sprite: Sprite2D = $CharacterSprite -enum State { IDLE, WALK, JUMP, LAND, ATTACK } +enum State { IDLE, WALK, JUMP, LAND, ATTACK, AIR_ATTACK } var anim_map := { - State.IDLE: "idle", - State.WALK: "idle", - State.JUMP: "jump", - State.LAND: "idle", - State.ATTACK: "挥砍", + State.IDLE: "warrior_idle", + State.WALK: "warrior_idle", + State.JUMP: "warrior_w", + State.LAND: "warrior_idle", + State.ATTACK: "warrior_a", + State.AIR_ATTACK: "warrior_a", } var attack_direction := Vector2.RIGHT var attack_lunge_time_left := 0.0 @@ -55,7 +59,7 @@ func handle_air_time(delta: float) -> void: height_speed -= GRAVITY * delta func handle_attack_time(delta: float) -> void: - if state != State.ATTACK: + if state != State.ATTACK and state != State.AIR_ATTACK: return velocity.y = 0.0 attack_time_left -= delta @@ -68,7 +72,7 @@ func handle_attack_time(delta: float) -> void: velocity.x = 0.0 func handle_movement() -> void: - if state == State.JUMP or state == State.ATTACK: + if state == State.JUMP or state == State.ATTACK or state == State.AIR_ATTACK: return if absf(velocity.x) > 0.0: state = State.WALK @@ -97,6 +101,9 @@ func can_jump() -> bool: func can_attack() -> bool: return state == State.IDLE or state == State.WALK +func can_air_attack() -> bool: + return state == State.IDLE or state == State.WALK + func start_jump() -> void: state = State.JUMP height_speed = jump_intensity @@ -112,3 +119,15 @@ func start_directional_attack(direction: Vector2) -> void: attack_time_left = attack_duration attack_lunge_time_left = attack_lunge_duration velocity = Vector2(attack_x * attack_lunge_speed, 0.0) + +func start_air_attack() -> void: + start_directional_air_attack(heading) + +func start_directional_air_attack(direction: Vector2) -> void: + var attack_x := -1.0 if direction.x < 0.0 else 1.0 + attack_direction = Vector2(attack_x, 0.0) + heading = Vector2.RIGHT if attack_x > 0.0 else Vector2.LEFT + state = State.AIR_ATTACK + attack_time_left = air_attack_duration + attack_lunge_time_left = air_attack_lunge_duration + velocity = Vector2(attack_x * air_attack_lunge_speed, 0.0) diff --git a/scenes/characters/player.gd b/scenes/characters/player.gd index b263fb4..77106a5 100644 --- a/scenes/characters/player.gd +++ b/scenes/characters/player.gd @@ -1,23 +1,106 @@ class_name Player extends Character +signal combo_window_changed(slots: Array) +signal combo_window_cleared(reason: String) +signal charge_changed(current: float, maximum: float, ready: bool, active: bool) +signal energy_changed(current: int, maximum: int) +signal health_changed(current: int, maximum: int) +signal skill_requested(skill_id: String) + +@export var combo_clear_display_time := 0.35 +@export var charge_duration := 1.1 +@export var max_health := 100 +@export var current_health := 100 +@export var max_energy := 10 +@export var current_energy := 0 + +var combo_window := ComboWindow.new() +var last_requested_skill_id := "" +var current_skill_animation := "" +var combo_clear_timer: Timer +var charge_value := 0.0 +var charge_ready := false +var charge_active := false +var _pending_combo_clear_reason := "" +var _charge_effect_time := 0.0 +var _charge_hold_symbol := "" +var _charge_hold_direction := Vector2.ZERO + + +func _ready() -> void: + combo_clear_timer = Timer.new() + combo_clear_timer.one_shot = true + combo_clear_timer.wait_time = combo_clear_display_time + combo_clear_timer.timeout.connect(flush_pending_combo_clear) + add_child(combo_clear_timer) + combo_window.window_cleared.connect(_on_combo_window_cleared) + _emit_combo_window_changed() + _emit_charge_changed() + _emit_health_changed() + _emit_energy_changed() + + +func _process(delta: float) -> void: + _update_charge(delta) + + +func _input(event: InputEvent) -> void: + if _handle_combo_key_event(event): + _mark_input_handled() + + +func _unhandled_input(event: InputEvent) -> void: + if _handle_combo_key_event(event): + _mark_input_handled() + + +func _handle_combo_key_event(event: InputEvent) -> bool: + var key_event := event as InputEventKey + if key_event == null or key_event.echo: + return false + if not key_event.pressed: + if _event_matches_key(key_event, KEY_A): + _finish_charge_hold("A") + return true + elif _event_matches_key(key_event, KEY_D): + _finish_charge_hold("D") + return true + return false + if _event_matches_key(key_event, KEY_W): + _submit_combo_input_from_event("W") + return true + elif _event_matches_key(key_event, KEY_A): + heading = Vector2.LEFT + var skill_id := _submit_combo_input_from_event("A") + if skill_id == "skill_a": + _begin_charge_hold("A", Vector2.LEFT) + return true + elif _event_matches_key(key_event, KEY_D): + heading = Vector2.RIGHT + var skill_id := _submit_combo_input_from_event("D") + if skill_id == "skill_d": + _begin_charge_hold("D", Vector2.RIGHT) + return true + elif _event_matches_key(key_event, KEY_S): + _submit_combo_input_from_event("S") + return true + elif _event_matches_key(key_event, KEY_SPACE): + _submit_combo_input_from_event("SP") + return true + return false + + func handle_input() -> void: - if Input.is_action_just_pressed("ui_left"): - judge_rhythm_action("left") - if can_attack(): - start_directional_attack(Vector2.LEFT) - return - if Input.is_action_just_pressed("ui_right"): - judge_rhythm_action("right") - if can_attack(): - start_directional_attack(Vector2.RIGHT) + if charge_active: + velocity = Vector2.ZERO return + _apply_horizontal_movement() if Input.is_action_just_pressed("jump"): judge_rhythm_action("jump") if can_jump(): start_jump() - if state == State.IDLE or state == State.WALK: - velocity.x = 0.0 + func set_heading() -> void: if velocity.x > 0.0: @@ -25,7 +108,407 @@ func set_heading() -> void: elif velocity.x < 0.0: heading = Vector2.LEFT -func judge_rhythm_action(action_name: String) -> void: + +func get_combo_slots() -> Array[String]: + return combo_window.get_slots() + + +func get_energy() -> int: + return current_energy + + +func get_max_energy() -> int: + return max_energy + + +func get_health() -> int: + return current_health + + +func get_max_health() -> int: + return max_health + + +func get_charge() -> float: + return charge_value + + +func get_max_charge() -> float: + return charge_duration + + +func is_charge_active() -> bool: + return charge_active + + +func is_charge_ready() -> bool: + return charge_ready + + +func submit_combo_input(symbol: String, forced_rating := "") -> String: + match symbol: + "W": + return _record_combo_symbol("W", "w", forced_rating) + "A": + return _record_combo_symbol("A", "a", forced_rating) + "D": + return _record_combo_symbol("D", "d", forced_rating) + "S": + return _record_combo_symbol("S", "s", forced_rating) + "SP": + return _record_combo_symbol("SP", "space", forced_rating) + return "" + + +func _record_combo_symbol(symbol: String, rhythm_action: String, forced_rating := "") -> String: + var rating := _rating_or_forced(judge_rhythm_action(rhythm_action), forced_rating) + _apply_energy_reward(str(rating.get("label", "perfect"))) + if not _record_rated_combo_input(symbol, rating): + if symbol == "A" or symbol == "D": + _cancel_missed_direction_action() + return "" + var resolved := InputResolver.resolve(combo_window) + if resolved.is_empty() and _pending_combo_clear_reason == "full": + resolved = _resolve_full_window_fallback(symbol) + if not resolved.is_empty(): + if not _execute_combo_skill(resolved): + return "" + if symbol == "SP" and not _is_projectile_space_chain() and _pending_combo_clear_reason.is_empty(): + _schedule_combo_clear("space") + return last_requested_skill_id if not resolved.is_empty() else "" + + +func _submit_combo_input_from_event(symbol: String) -> String: + return submit_combo_input(symbol) + + +func _event_matches_key(event: InputEventKey, key: Key) -> bool: + return event.physical_keycode == key or event.keycode == key + + +func _event_matches_any_key(event: InputEventKey, keys: Array[Key]) -> bool: + for key: Key in keys: + if _event_matches_key(event, key): + return true + return false + + +func _mark_input_handled() -> void: + var viewport := get_viewport() + if viewport != null: + viewport.set_input_as_handled() + + +func _record_combo_direction(symbol: String, direction: Vector2, rhythm_action: String, forced_rating := "") -> void: + heading = direction + var rating := _rating_or_forced(judge_rhythm_action(rhythm_action), forced_rating) + _record_rated_combo_input(symbol, rating) + if state == State.IDLE or state == State.WALK: + velocity.x = 0.0 + + +func _record_rated_combo_input(symbol: String, rating: Dictionary) -> bool: + if str(rating.get("label", "perfect")) == "miss": + _record_combo_input("Ø") + return false + _record_combo_input(symbol) + return true + + +func _record_combo_input(symbol: String) -> void: + if combo_window.has_pending_clear() or not _pending_combo_clear_reason.is_empty(): + if _pending_combo_clear_reason.begins_with("skill:") and not combo_window.has_pending_clear(): + _cancel_pending_combo_clear() + else: + flush_pending_combo_clear() + combo_window.record(symbol) + _emit_combo_window_changed() + var reason := combo_window.consume_pending_clear_reason() + if not reason.is_empty(): + _schedule_combo_clear(reason) + + +func _rating_or_forced(rating: Dictionary, forced_rating: String) -> Dictionary: + if forced_rating.is_empty(): + return rating + var forced := rating.duplicate() + forced["label"] = forced_rating + return forced + + +func _execute_combo_skill(skill: Dictionary) -> bool: + if not _spend_skill_energy(skill): + return false + last_requested_skill_id = str(skill.get("id", "")) + current_skill_animation = str(skill.get("animation", "warrior_idle")) + skill_requested.emit(last_requested_skill_id) + judge_rhythm_action(last_requested_skill_id) + _play_skill_animation(current_skill_animation, _skill_displacement_direction(skill)) + if bool(skill.get("projectile", false)): + _fire_projectile() + _emit_combo_window_changed() + if bool(skill.get("clear_window", false)): + _schedule_combo_clear("skill:%s" % last_requested_skill_id) + return true + + +func _play_skill_animation(animation_name: String, displacement_direction := Vector2.ZERO) -> void: + var player_animation := _get_animation_player() + anim_map[State.ATTACK] = animation_name + state = State.ATTACK + attack_time_left = _animation_length(animation_name) + if displacement_direction == Vector2.ZERO: + attack_lunge_time_left = 0.0 + velocity = Vector2.ZERO + else: + heading = displacement_direction + attack_lunge_time_left = attack_lunge_duration + velocity = Vector2(displacement_direction.x * attack_lunge_speed, 0.0) + if player_animation != null and player_animation.has_animation(animation_name): + player_animation.play(animation_name) + + +func _skill_displacement_direction(skill: Dictionary) -> Vector2: + match str(skill.get("displacement", "")): + "left": + return Vector2.LEFT + "right": + return Vector2.RIGHT + return Vector2.ZERO + + +func _apply_energy_reward(rating_label: String) -> void: + match rating_label: + "perfect": + _change_energy(2) + "good": + _change_energy(1) + + +func _spend_skill_energy(skill: Dictionary) -> bool: + var energy_cost := int(skill.get("energy_cost", 0)) + if energy_cost <= 0: + return true + if current_energy < energy_cost: + return false + _change_energy(-energy_cost) + return true + + +func _change_energy(delta: int) -> void: + var next_energy := clampi(current_energy + delta, 0, max_energy) + if next_energy == current_energy: + return + current_energy = next_energy + _emit_energy_changed() + + +func _begin_charge_hold(symbol: String, direction: Vector2) -> void: + _charge_hold_symbol = symbol + _charge_hold_direction = direction + + +func _finish_charge_hold(symbol: String) -> void: + if _charge_hold_symbol != symbol: + return + var release_ready := charge_active and charge_ready + var release_direction := _charge_hold_direction + var release_skill := "skill_a_charge_release" if symbol == "A" else "skill_d_charge_release" + _cancel_charge() + if release_ready: + _execute_charge_release(release_skill, release_direction) + + +func _update_charge(delta: float) -> void: + if _charge_hold_symbol.is_empty(): + return + if not charge_active: + if state != State.IDLE: + return + _start_charge() + if not charge_active: + return + state = State.IDLE + attack_time_left = 0.0 + attack_lunge_time_left = 0.0 + velocity = Vector2.ZERO + var player_animation := _get_animation_player() + if player_animation != null and player_animation.has_animation("warrior_idle") and player_animation.current_animation != "warrior_idle": + player_animation.play("warrior_idle") + charge_value = minf(charge_duration, charge_value + delta) + charge_ready = charge_value >= charge_duration + _update_charge_effect(delta) + _emit_charge_changed() + + +func _start_charge() -> void: + charge_active = true + charge_value = 0.0 + charge_ready = false + _charge_effect_time = 0.0 + _update_charge_effect(0.0) + _emit_charge_changed() + + +func _cancel_charge() -> void: + _charge_hold_symbol = "" + _charge_hold_direction = Vector2.ZERO + charge_active = false + charge_value = 0.0 + charge_ready = false + _set_charge_effect_visible(false) + _emit_charge_changed() + + +func _execute_charge_release(skill_id: String, direction: Vector2) -> void: + last_requested_skill_id = skill_id + current_skill_animation = "warrior_charge_release" + skill_requested.emit(last_requested_skill_id) + _play_skill_animation(current_skill_animation, direction) + + +func _update_charge_effect(delta: float) -> void: + var sprite := _get_charge_effect_sprite() + if sprite == null: + return + sprite.visible = charge_active + if not charge_active: + return + _charge_effect_time += delta + sprite.frame = int(_charge_effect_time * 12.0) % 5 + + +func _set_charge_effect_visible(is_visible: bool) -> void: + var sprite := _get_charge_effect_sprite() + if sprite != null: + sprite.visible = is_visible + + +func _get_charge_effect_sprite() -> Sprite2D: + if has_node("ChargeEffectSprite"): + return get_node("ChargeEffectSprite") as Sprite2D + return null + + +func _resolve_full_window_fallback(symbol: String) -> Dictionary: + if symbol != "A" and symbol != "D": + return {} + return InputResolver.resolve_pattern(symbol) + + +func _cancel_missed_direction_action() -> void: + velocity = Vector2.ZERO + attack_lunge_time_left = 0.0 + attack_time_left = 0.0 + state = State.IDLE + anim_map[State.ATTACK] = "warrior_a" + var player_animation := _get_animation_player() + if player_animation != null and player_animation.has_animation("warrior_idle"): + player_animation.play("warrior_idle") + + +func _is_projectile_space_chain() -> bool: + var pattern := combo_window.get_contiguous_pattern() + return pattern == "SSP" or pattern == "SSPSP" or pattern == "SSPSPSP" + + +func _apply_horizontal_movement() -> void: + if charge_active: + velocity.x = 0.0 + return + if state != State.IDLE and state != State.WALK: + return + var direction := 0.0 + if Input.is_action_pressed("player_a"): + direction -= 1.0 + if Input.is_action_pressed("player_d"): + direction += 1.0 + if direction < 0.0: + heading = Vector2.LEFT + elif direction > 0.0: + heading = Vector2.RIGHT + velocity.x = direction * speed + + +func _animation_length(animation_name: String) -> float: + var player_animation := _get_animation_player() + if player_animation != null and player_animation.has_animation(animation_name): + return maxf(0.1, player_animation.get_animation(animation_name).length) + return attack_duration + + +func _get_animation_player() -> AnimationPlayer: + if animation_player == null and has_node("AnimationPlayer"): + animation_player = get_node("AnimationPlayer") as AnimationPlayer + return animation_player + + +func _fire_projectile() -> void: + var projectile := PlayerProjectile.new() + projectile.direction = heading + projectile.global_position = global_position + Vector2(heading.x * 36.0, -30.0) + var parent := get_parent() + if parent != null: + parent.add_child(projectile) + else: + add_child(projectile) + projectile.add_to_group("player_projectiles") + + +func _cancel_pending_combo_clear() -> void: + _pending_combo_clear_reason = "" + if combo_clear_timer != null: + combo_clear_timer.stop() + + +func _schedule_combo_clear(reason: String) -> void: + _pending_combo_clear_reason = reason + if combo_clear_timer == null: + return + combo_clear_timer.stop() + combo_clear_timer.wait_time = combo_clear_display_time + combo_clear_timer.start() + + +func flush_pending_combo_clear() -> void: + var reason := _pending_combo_clear_reason + if reason.is_empty(): + reason = combo_window.consume_pending_clear_reason() + else: + combo_window.consume_pending_clear_reason() + if reason.is_empty(): + return + _pending_combo_clear_reason = "" + if combo_clear_timer != null: + combo_clear_timer.stop() + combo_window.clear(reason) + + +func _on_combo_window_cleared(reason: String) -> void: + combo_window_cleared.emit(reason) + _emit_combo_window_changed() + + +func _emit_combo_window_changed() -> void: + combo_window_changed.emit(combo_window.get_slots()) + + +func _emit_charge_changed() -> void: + charge_changed.emit(charge_value, charge_duration, charge_ready, charge_active) + + +func _emit_energy_changed() -> void: + energy_changed.emit(current_energy, max_energy) + + +func _emit_health_changed() -> void: + health_changed.emit(current_health, max_health) + + +func judge_rhythm_action(action_name: String) -> Dictionary: + if not is_inside_tree(): + return {"label": "perfect"} var conductor: Node = get_tree().get_first_node_in_group("rhythm_conductor") if conductor != null and conductor.has_method("judge_action"): - conductor.call("judge_action", action_name) + return conductor.call("judge_action", action_name) as Dictionary + return {"label": "perfect"} diff --git a/scenes/characters/player.tscn b/scenes/characters/player.tscn index 5d5a322..a12ddd0 100644 --- a/scenes/characters/player.tscn +++ b/scenes/characters/player.tscn @@ -1,18 +1,17 @@ -[gd_scene load_steps=10 format=3] +[gd_scene format=3 uid="uid://cs3s5wy1melul"] -[ext_resource type="Script" path="res://scenes/characters/player.gd" id="1_player_script"] -[ext_resource type="Texture2D" path="res://assets/art/characters/jump.png" id="2_jump_texture"] -[ext_resource type="Texture2D" path="res://assets/art/characters/katana_attack_sheathe.png" id="3_slash_texture"] -[ext_resource type="Texture2D" path="res://assets/art/characters/player_idle.png" id="4_idle_texture"] +[ext_resource type="Script" uid="uid://cwp1u2srtj5ko" path="res://scenes/characters/player.gd" id="1_player_script"] +[ext_resource type="Texture2D" uid="uid://bbkamgcdsw5g6" path="res://assets/art/characters/warrior_man_sheet.png" id="2_yewv4"] +[ext_resource type="Texture2D" uid="uid://womoel71g8ae" path="res://assets/art/characters/warrior_woman_sheet.png" id="3_dyp2m"] +[ext_resource type="Texture2D" uid="uid://1p2uqgg1jole" path="res://assets/art/effects/effect_hp_mp_sheet.png" id="4_atpat"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_player"] size = Vector2(16, 36) -[sub_resource type="Animation" id="Animation_idle"] -resource_name = "idle" -length = 1.0 -loop_mode = 1 -step = 0.1 +[sub_resource type="Animation" id="Animation_76oj4"] +resource_name = "warrior_a" +length = 0.5833333 +step = 0.083333336 tracks/0/type = "value" tracks/0/imported = false tracks/0/enabled = true @@ -23,7 +22,7 @@ tracks/0/keys = { "times": PackedFloat32Array(0), "transitions": PackedFloat32Array(1), "update": 1, -"values": [ExtResource("4_idle_texture")] +"values": [ExtResource("2_yewv4")] } tracks/1/type = "value" tracks/1/imported = false @@ -35,7 +34,7 @@ tracks/1/keys = { "times": PackedFloat32Array(0), "transitions": PackedFloat32Array(1), "update": 1, -"values": [10] +"values": [16] } tracks/2/type = "value" tracks/2/imported = false @@ -47,138 +46,7 @@ tracks/2/keys = { "times": PackedFloat32Array(0), "transitions": PackedFloat32Array(1), "update": 1, -"values": [1] -} -tracks/3/type = "value" -tracks/3/imported = false -tracks/3/enabled = true -tracks/3/path = NodePath("CharacterSprite:offset") -tracks/3/interp = 1 -tracks/3/loop_wrap = true -tracks/3/keys = { -"times": PackedFloat32Array(0), -"transitions": PackedFloat32Array(1), -"update": 1, -"values": [Vector2(-24, -40)] -} -tracks/4/type = "value" -tracks/4/imported = false -tracks/4/enabled = true -tracks/4/path = NodePath("CharacterSprite:frame") -tracks/4/interp = 1 -tracks/4/loop_wrap = true -tracks/4/keys = { -"times": PackedFloat32Array(0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9), -"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1), -"update": 1, -"values": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] -} - -[sub_resource type="Animation" id="Animation_jump"] -resource_name = "jump" -length = 0.36 -loop_mode = 1 -step = 0.06 -tracks/0/type = "value" -tracks/0/imported = false -tracks/0/enabled = true -tracks/0/path = NodePath("CharacterSprite:texture") -tracks/0/interp = 1 -tracks/0/loop_wrap = true -tracks/0/keys = { -"times": PackedFloat32Array(0), -"transitions": PackedFloat32Array(1), -"update": 1, -"values": [ExtResource("2_jump_texture")] -} -tracks/1/type = "value" -tracks/1/imported = false -tracks/1/enabled = true -tracks/1/path = NodePath("CharacterSprite:hframes") -tracks/1/interp = 1 -tracks/1/loop_wrap = true -tracks/1/keys = { -"times": PackedFloat32Array(0), -"transitions": PackedFloat32Array(1), -"update": 1, -"values": [6] -} -tracks/2/type = "value" -tracks/2/imported = false -tracks/2/enabled = true -tracks/2/path = NodePath("CharacterSprite:vframes") -tracks/2/interp = 1 -tracks/2/loop_wrap = true -tracks/2/keys = { -"times": PackedFloat32Array(0), -"transitions": PackedFloat32Array(1), -"update": 1, -"values": [1] -} -tracks/3/type = "value" -tracks/3/imported = false -tracks/3/enabled = true -tracks/3/path = NodePath("CharacterSprite:offset") -tracks/3/interp = 1 -tracks/3/loop_wrap = true -tracks/3/keys = { -"times": PackedFloat32Array(0), -"transitions": PackedFloat32Array(1), -"update": 1, -"values": [Vector2(-24, -44)] -} -tracks/4/type = "value" -tracks/4/imported = false -tracks/4/enabled = true -tracks/4/path = NodePath("CharacterSprite:frame") -tracks/4/interp = 1 -tracks/4/loop_wrap = true -tracks/4/keys = { -"times": PackedFloat32Array(0, 0.06, 0.12, 0.18, 0.24, 0.3), -"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1), -"update": 1, -"values": [0, 1, 2, 3, 4, 5] -} - -[sub_resource type="Animation" id="Animation_slash"] -resource_name = "挥砍" -length = 0.4 -step = 0.04 -tracks/0/type = "value" -tracks/0/imported = false -tracks/0/enabled = true -tracks/0/path = NodePath("CharacterSprite:texture") -tracks/0/interp = 1 -tracks/0/loop_wrap = true -tracks/0/keys = { -"times": PackedFloat32Array(0), -"transitions": PackedFloat32Array(1), -"update": 1, -"values": [ExtResource("3_slash_texture")] -} -tracks/1/type = "value" -tracks/1/imported = false -tracks/1/enabled = true -tracks/1/path = NodePath("CharacterSprite:hframes") -tracks/1/interp = 1 -tracks/1/loop_wrap = true -tracks/1/keys = { -"times": PackedFloat32Array(0), -"transitions": PackedFloat32Array(1), -"update": 1, -"values": [10] -} -tracks/2/type = "value" -tracks/2/imported = false -tracks/2/enabled = true -tracks/2/path = NodePath("CharacterSprite:vframes") -tracks/2/interp = 1 -tracks/2/loop_wrap = true -tracks/2/keys = { -"times": PackedFloat32Array(0), -"transitions": PackedFloat32Array(1), -"update": 1, -"values": [1] +"values": [25] } tracks/3/type = "value" tracks/3/imported = false @@ -199,51 +67,708 @@ tracks/4/path = NodePath("CharacterSprite:frame") tracks/4/interp = 1 tracks/4/loop_wrap = true tracks/4/keys = { -"times": PackedFloat32Array(0, 0.04, 0.08, 0.12, 0.16, 0.2, 0.24, 0.28, 0.32, 0.36), +"times": PackedFloat32Array(0, 0.083333336, 0.16666667, 0.25, 0.33333334, 0.41666666, 0.5), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [144, 145, 146, 147, 148, 149, 150] +} + +[sub_resource type="Animation" id="Animation_yewv4"] +resource_name = "warrior_a_space" +length = 0.8333333 +step = 0.083333336 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("CharacterSprite:texture") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [ExtResource("2_yewv4")] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("CharacterSprite:hframes") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [16] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("CharacterSprite:vframes") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [25] +} +tracks/3/type = "value" +tracks/3/imported = false +tracks/3/enabled = true +tracks/3/path = NodePath("CharacterSprite:offset") +tracks/3/interp = 1 +tracks/3/loop_wrap = true +tracks/3/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [Vector2(-40, -48)] +} +tracks/4/type = "value" +tracks/4/imported = false +tracks/4/enabled = true +tracks/4/path = NodePath("CharacterSprite:frame") +tracks/4/interp = 1 +tracks/4/loop_wrap = true +tracks/4/keys = { +"times": PackedFloat32Array(0, 0.083333336, 0.16666667, 0.25, 0.33333334, 0.41666666, 0.5, 0.5833333, 0.6666667, 0.75), "transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1), "update": 1, -"values": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +"values": [256, 257, 258, 259, 260, 261, 262, 263, 264, 265] } -[sub_resource type="AnimationLibrary" id="AnimationLibrary_player"] +[sub_resource type="Animation" id="Animation_2l4js"] +resource_name = "warrior_a_space_space" +step = 0.083333336 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("CharacterSprite:texture") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [ExtResource("2_yewv4")] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("CharacterSprite:hframes") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [16] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("CharacterSprite:vframes") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [25] +} +tracks/3/type = "value" +tracks/3/imported = false +tracks/3/enabled = true +tracks/3/path = NodePath("CharacterSprite:offset") +tracks/3/interp = 1 +tracks/3/loop_wrap = true +tracks/3/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [Vector2(-40, -48)] +} +tracks/4/type = "value" +tracks/4/imported = false +tracks/4/enabled = true +tracks/4/path = NodePath("CharacterSprite:frame") +tracks/4/interp = 1 +tracks/4/loop_wrap = true +tracks/4/keys = { +"times": PackedFloat32Array(0, 0.083333336, 0.16666667, 0.25, 0.33333334, 0.41666666, 0.5, 0.5833333, 0.6666667, 0.75, 0.8333333, 0.9166667), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235] +} + +[sub_resource type="Animation" id="Animation_dyp2m"] +resource_name = "warrior_aa" +length = 0.41666666 +step = 0.083333336 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("CharacterSprite:texture") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [ExtResource("2_yewv4")] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("CharacterSprite:hframes") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [16] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("CharacterSprite:vframes") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [25] +} +tracks/3/type = "value" +tracks/3/imported = false +tracks/3/enabled = true +tracks/3/path = NodePath("CharacterSprite:offset") +tracks/3/interp = 1 +tracks/3/loop_wrap = true +tracks/3/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [Vector2(-40, -48)] +} +tracks/4/type = "value" +tracks/4/imported = false +tracks/4/enabled = true +tracks/4/path = NodePath("CharacterSprite:frame") +tracks/4/interp = 1 +tracks/4/loop_wrap = true +tracks/4/keys = { +"times": PackedFloat32Array(0, 0.083333336, 0.16666667, 0.25, 0.33333334), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1), +"update": 1, +"values": [160, 161, 162, 163, 164] +} + +[sub_resource type="Animation" id="Animation_atpat"] +resource_name = "warrior_aaa" +length = 0.5714286 +step = 0.071428575 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("CharacterSprite:texture") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [ExtResource("2_yewv4")] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("CharacterSprite:hframes") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [16] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("CharacterSprite:vframes") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [25] +} +tracks/3/type = "value" +tracks/3/imported = false +tracks/3/enabled = true +tracks/3/path = NodePath("CharacterSprite:offset") +tracks/3/interp = 1 +tracks/3/loop_wrap = true +tracks/3/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [Vector2(-40, -48)] +} +tracks/4/type = "value" +tracks/4/imported = false +tracks/4/enabled = true +tracks/4/path = NodePath("CharacterSprite:frame") +tracks/4/interp = 1 +tracks/4/loop_wrap = true +tracks/4/keys = { +"times": PackedFloat32Array(0, 0.071428575, 0.14285715, 0.21428572, 0.2857143, 0.35714287, 0.42857143, 0.5), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [176, 177, 178, 179, 180, 181, 182, 183] +} + +[sub_resource type="Animation" id="Animation_jk2m4"] +resource_name = "warrior_charge_release" +length = 1.3333334 +step = 0.083333336 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("CharacterSprite:texture") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [ExtResource("3_dyp2m")] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("CharacterSprite:hframes") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [16] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("CharacterSprite:vframes") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [25] +} +tracks/3/type = "value" +tracks/3/imported = false +tracks/3/enabled = true +tracks/3/path = NodePath("CharacterSprite:offset") +tracks/3/interp = 1 +tracks/3/loop_wrap = true +tracks/3/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [Vector2(-40, -48)] +} +tracks/4/type = "value" +tracks/4/imported = false +tracks/4/enabled = true +tracks/4/path = NodePath("CharacterSprite:frame") +tracks/4/interp = 1 +tracks/4/loop_wrap = true +tracks/4/keys = { +"times": PackedFloat32Array(0, 0.083333336, 0.16666667, 0.25, 0.33333334, 0.41666666, 0.5, 0.5833333, 0.6666667, 0.75, 0.8333333, 0.9166667, 1, 1.0833334, 1.1666666, 1.25), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207] +} + +[sub_resource type="Animation" id="Animation_kqtwu"] +resource_name = "warrior_idle" +length = 0.8 +loop_mode = 1 +step = 0.1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("CharacterSprite:texture") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [ExtResource("2_yewv4")] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("CharacterSprite:hframes") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [16] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("CharacterSprite:vframes") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [25] +} +tracks/3/type = "value" +tracks/3/imported = false +tracks/3/enabled = true +tracks/3/path = NodePath("CharacterSprite:offset") +tracks/3/interp = 1 +tracks/3/loop_wrap = true +tracks/3/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [Vector2(-40, -48)] +} +tracks/4/type = "value" +tracks/4/imported = false +tracks/4/enabled = true +tracks/4/path = NodePath("CharacterSprite:frame") +tracks/4/interp = 1 +tracks/4/loop_wrap = true +tracks/4/keys = { +"times": PackedFloat32Array(0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [0, 1, 2, 3, 4, 5, 6, 7] +} + +[sub_resource type="Animation" id="Animation_6eyoc"] +resource_name = "warrior_s" +length = 0.71428573 +step = 0.071428575 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("CharacterSprite:texture") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [ExtResource("2_yewv4")] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("CharacterSprite:hframes") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [16] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("CharacterSprite:vframes") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [25] +} +tracks/3/type = "value" +tracks/3/imported = false +tracks/3/enabled = true +tracks/3/path = NodePath("CharacterSprite:offset") +tracks/3/interp = 1 +tracks/3/loop_wrap = true +tracks/3/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [Vector2(-40, -48)] +} +tracks/4/type = "value" +tracks/4/imported = false +tracks/4/enabled = true +tracks/4/path = NodePath("CharacterSprite:frame") +tracks/4/interp = 1 +tracks/4/loop_wrap = true +tracks/4/keys = { +"times": PackedFloat32Array(0, 0.071428575, 0.14285715, 0.21428572, 0.2857143, 0.35714287, 0.42857143, 0.5, 0.5714286, 0.64285713), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [128, 129, 130, 131, 132, 133, 134, 135, 136, 137] +} + +[sub_resource type="Animation" id="Animation_eojnx"] +resource_name = "warrior_s_projectile" +length = 0.8125 +step = 0.0625 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("CharacterSprite:texture") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [ExtResource("2_yewv4")] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("CharacterSprite:hframes") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [16] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("CharacterSprite:vframes") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [25] +} +tracks/3/type = "value" +tracks/3/imported = false +tracks/3/enabled = true +tracks/3/path = NodePath("CharacterSprite:offset") +tracks/3/interp = 1 +tracks/3/loop_wrap = true +tracks/3/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [Vector2(-40, -48)] +} +tracks/4/type = "value" +tracks/4/imported = false +tracks/4/enabled = true +tracks/4/path = NodePath("CharacterSprite:frame") +tracks/4/interp = 1 +tracks/4/loop_wrap = true +tracks/4/keys = { +"times": PackedFloat32Array(0, 0.0625, 0.125, 0.1875, 0.25, 0.3125, 0.375, 0.4375, 0.5, 0.5625, 0.625, 0.6875, 0.75), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220] +} + +[sub_resource type="Animation" id="Animation_qgnko"] +resource_name = "warrior_w" +length = 0.5 +step = 0.083333336 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("CharacterSprite:texture") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [ExtResource("2_yewv4")] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("CharacterSprite:hframes") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [16] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("CharacterSprite:vframes") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [25] +} +tracks/3/type = "value" +tracks/3/imported = false +tracks/3/enabled = true +tracks/3/path = NodePath("CharacterSprite:offset") +tracks/3/interp = 1 +tracks/3/loop_wrap = true +tracks/3/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [Vector2(-40, -48)] +} +tracks/4/type = "value" +tracks/4/imported = false +tracks/4/enabled = true +tracks/4/path = NodePath("CharacterSprite:frame") +tracks/4/interp = 1 +tracks/4/loop_wrap = true +tracks/4/keys = { +"times": PackedFloat32Array(0, 0.083333336, 0.16666667, 0.25, 0.33333334, 0.41666666), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1), +"update": 1, +"values": [80, 81, 82, 83, 84, 85] +} + +[sub_resource type="Animation" id="Animation_7vyk4"] +resource_name = "warrior_wa" +length = 0.41666666 +step = 0.083333336 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("CharacterSprite:texture") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [ExtResource("2_yewv4")] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("CharacterSprite:hframes") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [16] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("CharacterSprite:vframes") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [25] +} +tracks/3/type = "value" +tracks/3/imported = false +tracks/3/enabled = true +tracks/3/path = NodePath("CharacterSprite:offset") +tracks/3/interp = 1 +tracks/3/loop_wrap = true +tracks/3/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [Vector2(-40, -48)] +} +tracks/4/type = "value" +tracks/4/imported = false +tracks/4/enabled = true +tracks/4/path = NodePath("CharacterSprite:frame") +tracks/4/interp = 1 +tracks/4/loop_wrap = true +tracks/4/keys = { +"times": PackedFloat32Array(0, 0.083333336, 0.16666667, 0.25, 0.33333334), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1), +"update": 1, +"values": [96, 97, 98, 99, 100] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_2l4js"] _data = { -"idle": SubResource("Animation_idle"), -"jump": SubResource("Animation_jump"), -"挥砍": SubResource("Animation_slash") +&"warrior_a": SubResource("Animation_76oj4"), +&"warrior_a_space": SubResource("Animation_yewv4"), +&"warrior_a_space_space": SubResource("Animation_2l4js"), +&"warrior_aa": SubResource("Animation_dyp2m"), +&"warrior_aaa": SubResource("Animation_atpat"), +&"warrior_charge_release": SubResource("Animation_jk2m4"), +&"warrior_idle": SubResource("Animation_kqtwu"), +&"warrior_s": SubResource("Animation_6eyoc"), +&"warrior_s_projectile": SubResource("Animation_eojnx"), +&"warrior_w": SubResource("Animation_qgnko"), +&"warrior_wa": SubResource("Animation_7vyk4") } -[node name="Player" type="CharacterBody2D"] -collision_layer = 2 -collision_mask = 1 -safe_margin = 0.001 -floor_snap_length = 0.0 +[node name="Player" type="CharacterBody2D" unique_id=1029375298] scale = Vector2(4, 4) +collision_layer = 2 +floor_snap_length = 0.0 +safe_margin = 0.001 script = ExtResource("1_player_script") -speed = 180.0 -jump_intensity = 304.056 -attack_duration = 0.4 -attack_lunge_duration = 0.18 -attack_lunge_speed = 220.0 -[node name="CharacterSprite" type="Sprite2D" parent="."] -texture = ExtResource("4_idle_texture") +[node name="CharacterSprite" type="Sprite2D" parent="." unique_id=1175595770] +texture = ExtResource("2_yewv4") centered = false -offset = Vector2(-24, -40) -hframes = 10 -vframes = 1 +offset = Vector2(-40, -48) +hframes = 16 +vframes = 25 -[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=1167515641] position = Vector2(0, -18) shape = SubResource("RectangleShape2D_player") -[node name="AnimationPlayer" type="AnimationPlayer" parent="."] -libraries = { -"": SubResource("AnimationLibrary_player") -} -autoplay = "idle" +[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=822598049] +libraries/ = SubResource("AnimationLibrary_2l4js") +autoplay = &"warrior_idle" -[node name="Camera2D" type="Camera2D" parent="."] +[node name="Camera2D" type="Camera2D" parent="." unique_id=1607685219] position = Vector2(0, -37.5) scale = Vector2(0.25, 0.25) zoom = Vector2(1.5, 1.5) -enabled = true + +[node name="ChargeEffectSprite" type="Sprite2D" parent="." unique_id=1049185311] +visible = false +z_index = 2 +position = Vector2(0, -8) +texture = ExtResource("4_atpat") +hframes = 5 +vframes = 2 diff --git a/scenes/combat/combo_window.gd b/scenes/combat/combo_window.gd new file mode 100644 index 0000000..dae5b1c --- /dev/null +++ b/scenes/combat/combo_window.gd @@ -0,0 +1,55 @@ +class_name ComboWindow +extends RefCounted + +signal window_cleared(reason: String) + +const SIZE := 4 + +var slots: Array[String] = [] +var pending_clear_reason := "" + + +func record(input: String) -> void: + if input.is_empty(): + return + slots.append(input) + if slots.size() >= SIZE: + pending_clear_reason = "full" + + +func get_slots() -> Array[String]: + return slots.duplicate() + + +func has_pending_clear() -> bool: + return not pending_clear_reason.is_empty() + + +func consume_pending_clear_reason() -> String: + var reason := pending_clear_reason + pending_clear_reason = "" + return reason + + +func get_pattern() -> String: + var pattern := "" + for slot: String in slots: + if slot != "Ø": + pattern += slot + return pattern + + +func get_contiguous_pattern() -> String: + var pattern := "" + for index: int in range(slots.size() - 1, -1, -1): + var slot := slots[index] + if slot == "Ø": + break + pattern = slot + pattern + return pattern + + +func clear(reason := "") -> void: + slots.clear() + pending_clear_reason = "" + window_cleared.emit(reason) diff --git a/scenes/combat/combo_window.gd.uid b/scenes/combat/combo_window.gd.uid new file mode 100644 index 0000000..6087c7a --- /dev/null +++ b/scenes/combat/combo_window.gd.uid @@ -0,0 +1 @@ +uid://dtguxwnh02f6g diff --git a/scenes/combat/input_resolver.gd b/scenes/combat/input_resolver.gd new file mode 100644 index 0000000..843654b --- /dev/null +++ b/scenes/combat/input_resolver.gd @@ -0,0 +1,151 @@ +class_name InputResolver +extends RefCounted + +const SKILLS := { + "A": { + "type": "skill", + "id": "skill_a", + "animation": "warrior_a", + "displacement": "left", + "clear_window": false, + }, + "D": { + "type": "skill", + "id": "skill_d", + "animation": "warrior_a", + "displacement": "right", + "clear_window": false, + }, + "WA": { + "type": "skill", + "id": "skill_wa", + "animation": "warrior_wa", + "displacement": "left", + "clear_window": false, + }, + "WD": { + "type": "skill", + "id": "skill_wd", + "animation": "warrior_wa", + "displacement": "right", + "clear_window": false, + }, + "AA": { + "type": "skill", + "id": "skill_aa", + "animation": "warrior_aa", + "displacement": "left", + "clear_window": false, + }, + "DD": { + "type": "skill", + "id": "skill_dd", + "animation": "warrior_aa", + "displacement": "right", + "clear_window": false, + }, + "AAA": { + "type": "skill", + "id": "skill_aaa", + "animation": "warrior_aaa", + "displacement": "left", + "clear_window": false, + }, + "DDD": { + "type": "skill", + "id": "skill_ddd", + "animation": "warrior_aaa", + "displacement": "right", + "clear_window": false, + }, + "ASP": { + "type": "skill", + "id": "skill_a_space", + "animation": "warrior_a_space", + "displacement": "left", + "clear_window": true, + }, + "DSP": { + "type": "skill", + "id": "skill_d_space", + "animation": "warrior_a_space", + "displacement": "right", + "clear_window": true, + }, + "ASPSP": { + "type": "skill", + "id": "skill_a_space_space", + "animation": "warrior_a_space_space", + "displacement": "left", + "clear_window": true, + }, + "DSPSP": { + "type": "skill", + "id": "skill_d_space_space", + "animation": "warrior_a_space_space", + "displacement": "right", + "clear_window": true, + }, + "AASP": { + "type": "skill", + "id": "skill_aa_space", + "animation": "warrior_a_space_space", + "displacement": "left", + "clear_window": true, + }, + "ADSP": { + "type": "skill", + "id": "skill_ad_space", + "animation": "warrior_a_space_space", + "displacement": "right", + "clear_window": true, + }, + "DASP": { + "type": "skill", + "id": "skill_da_space", + "animation": "warrior_a_space_space", + "displacement": "left", + "clear_window": true, + }, + "DDSP": { + "type": "skill", + "id": "skill_dd_space", + "animation": "warrior_a_space_space", + "displacement": "right", + "clear_window": true, + }, + "SSP": { + "type": "skill", + "id": "skill_s_projectile_1", + "animation": "warrior_s_projectile", + "projectile": true, + "energy_cost": 3, + "clear_window": false, + }, + "SSPSP": { + "type": "skill", + "id": "skill_s_projectile_2", + "animation": "warrior_s_projectile", + "projectile": true, + "energy_cost": 2, + "clear_window": false, + }, + "SSPSPSP": { + "type": "skill", + "id": "skill_s_projectile_3", + "animation": "warrior_s_projectile", + "projectile": true, + "energy_cost": 1, + "clear_window": false, + }, +} + + +static func resolve(window: ComboWindow) -> Dictionary: + return resolve_pattern(window.get_contiguous_pattern()) + + +static func resolve_pattern(pattern: String) -> Dictionary: + if not SKILLS.has(pattern): + return {} + return SKILLS[pattern].duplicate() diff --git a/scenes/combat/input_resolver.gd.uid b/scenes/combat/input_resolver.gd.uid new file mode 100644 index 0000000..54d5078 --- /dev/null +++ b/scenes/combat/input_resolver.gd.uid @@ -0,0 +1 @@ +uid://cyhq381jiyo42 diff --git a/scenes/combat/player_projectile.gd b/scenes/combat/player_projectile.gd new file mode 100644 index 0000000..a9cdfac --- /dev/null +++ b/scenes/combat/player_projectile.gd @@ -0,0 +1,42 @@ +class_name PlayerProjectile +extends Node2D + +const EFFECT_TEXTURE := preload("res://assets/art/effects/effect_sheet.png") +const FRAME_COUNT := 4 +const FRAME_TIME := 0.06 +const LIFE_TIME := 1.2 + +var direction := Vector2.RIGHT +var speed := 360.0 +var _age := 0.0 +var _sprite: Sprite2D + + +func _init() -> void: + _create_sprite() + + +func _ready() -> void: + add_to_group("player_projectiles") + if _sprite == null: + _create_sprite() + + +func _create_sprite() -> void: + _sprite = Sprite2D.new() + _sprite.texture = EFFECT_TEXTURE + _sprite.hframes = 6 + _sprite.vframes = 2 + _sprite.centered = true + _sprite.scale = Vector2(2.0, 2.0) + add_child(_sprite) + + +func _process(delta: float) -> void: + _age += delta + position += direction.normalized() * speed * delta + _sprite.frame = int(_age / FRAME_TIME) % FRAME_COUNT + if direction.x < 0.0: + _sprite.flip_h = true + if _age >= LIFE_TIME: + queue_free() diff --git a/scenes/combat/player_projectile.gd.uid b/scenes/combat/player_projectile.gd.uid new file mode 100644 index 0000000..28a1942 --- /dev/null +++ b/scenes/combat/player_projectile.gd.uid @@ -0,0 +1 @@ +uid://bwjk27wxb6p20 diff --git a/scenes/main/main.gd b/scenes/main/main.gd index 10528f7..034a5d5 100644 --- a/scenes/main/main.gd +++ b/scenes/main/main.gd @@ -3,10 +3,43 @@ extends Node2D @onready var rhythm_conductor: Node = $RhythmConductor @onready var rhythm_track: Control = $RhythmFeedback/RhythmTrack @onready var rhythm_feedback_label: Label = $RhythmFeedback/JudgementLabel +@onready var player: Node = $Player @onready var center_base: TextureRect = $RhythmFeedback/RhythmTrack/CenterBase @onready var center_flash: TextureRect = $RhythmFeedback/RhythmTrack/CenterFlash @onready var left_mover: TextureRect = $RhythmFeedback/RhythmTrack/LeftMover @onready var right_mover: TextureRect = $RhythmFeedback/RhythmTrack/RightMover +@onready var combo_skill_label: Label = $RhythmFeedback/ComboSkillLabel +@onready var health_bar: ProgressBar = $RhythmFeedback/StatusBars/HealthBar +@onready var charge_bar: ProgressBar = $RhythmFeedback/StatusBars/ChargeBar +@onready var energy_segments: Array[Panel] = [ + $RhythmFeedback/StatusBars/EnergyBar/Segment0, + $RhythmFeedback/StatusBars/EnergyBar/Segment1, + $RhythmFeedback/StatusBars/EnergyBar/Segment2, + $RhythmFeedback/StatusBars/EnergyBar/Segment3, + $RhythmFeedback/StatusBars/EnergyBar/Segment4, + $RhythmFeedback/StatusBars/EnergyBar/Segment5, + $RhythmFeedback/StatusBars/EnergyBar/Segment6, + $RhythmFeedback/StatusBars/EnergyBar/Segment7, + $RhythmFeedback/StatusBars/EnergyBar/Segment8, + $RhythmFeedback/StatusBars/EnergyBar/Segment9, +] +@onready var combo_slot_panels: Array[PanelContainer] = [ + $RhythmFeedback/ComboWindow/Slot0, + $RhythmFeedback/ComboWindow/Slot1, + $RhythmFeedback/ComboWindow/Slot2, + $RhythmFeedback/ComboWindow/Slot3, +] +@onready var combo_key_labels: Array[Label] = [ + $RhythmFeedback/ComboWindow/Slot0/Key, + $RhythmFeedback/ComboWindow/Slot1/Key, + $RhythmFeedback/ComboWindow/Slot2/Key, + $RhythmFeedback/ComboWindow/Slot3/Key, +] + +var combo_clear_tween: Tween +var combo_clear_flash := 0.0 +var charge_bar_ready := false +var charge_flash := 0.0 var track_center := Vector2.ZERO var left_mover_start := Vector2.ZERO @@ -21,12 +54,33 @@ func _ready() -> void: _cache_rhythm_track_layout() rhythm_conductor.action_judged.connect(_on_rhythm_action_judged) rhythm_conductor.beat.connect(_on_rhythm_beat) + if player.has_signal("combo_window_changed"): + player.connect("combo_window_changed", _on_combo_window_changed) + if player.has_signal("combo_window_cleared"): + player.connect("combo_window_cleared", _on_combo_window_cleared) + if player.has_signal("skill_requested"): + player.connect("skill_requested", _on_skill_requested) + if player.has_signal("energy_changed"): + player.connect("energy_changed", _on_energy_changed) + if player.has_signal("health_changed"): + player.connect("health_changed", _on_health_changed) + if player.has_signal("charge_changed"): + player.connect("charge_changed", _on_charge_changed) rhythm_feedback_label.text = "READY" + _on_combo_window_changed([]) + if player.has_method("get_energy") and player.has_method("get_max_energy"): + _on_energy_changed(player.call("get_energy"), player.call("get_max_energy")) + if player.has_method("get_health") and player.has_method("get_max_health"): + _on_health_changed(player.call("get_health"), player.call("get_max_health")) + if player.has_method("get_charge") and player.has_method("get_max_charge") and player.has_method("is_charge_ready") and player.has_method("is_charge_active"): + _on_charge_changed(player.call("get_charge"), player.call("get_max_charge"), player.call("is_charge_ready"), player.call("is_charge_active")) _update_rhythm_track(0.0) func _process(delta: float) -> void: _update_rhythm_track(delta) + _update_combo_clear_animation(delta) + _update_charge_bar_flash(delta) if feedback_flash > 0.0: feedback_flash = maxf(0.0, feedback_flash - delta * 4.0) rhythm_feedback_label.scale = Vector2.ONE * (1.0 + feedback_flash * 0.18) @@ -50,6 +104,92 @@ func _on_rhythm_beat(_position: int) -> void: beat_flash = 1.0 +func _on_combo_window_changed(slots: Array) -> void: + for index: int in range(combo_key_labels.size()): + var filled := index < slots.size() + var label := combo_key_labels[index] + var panel := combo_slot_panels[index] + label.text = str(slots[index]) if filled else "·" + label.modulate = Color(1.0, 1.0, 1.0, 1.0 if filled else 0.32) + panel.modulate = Color(1.0, 1.0, 1.0, 1.0 if filled else 0.48) + if filled: + _pulse_combo_slot(panel) + + +func _on_combo_window_cleared(_reason: String) -> void: + _play_combo_clear_animation() + + +func _on_skill_requested(skill_id: String) -> void: + combo_skill_label.text = _format_skill_name(skill_id) + + +func _on_energy_changed(current: int, maximum: int) -> void: + var filled_segments := clampi(current, 0, min(maximum, energy_segments.size())) + for index: int in range(energy_segments.size()): + var filled := index < filled_segments + var panel := energy_segments[index] + panel.modulate = Color(1.0, 1.0, 1.0, 1.0 if filled else 0.38) + + +func _on_health_changed(current: int, maximum: int) -> void: + health_bar.max_value = max(1, maximum) + health_bar.value = clampi(current, 0, maximum) + + +func _on_charge_changed(current: float, maximum: float, ready: bool, active: bool) -> void: + charge_bar.max_value = maxf(0.01, maximum) + charge_bar.value = clampf(current, 0.0, maximum) + charge_bar_ready = ready and active + if charge_bar_ready: + return + charge_bar.modulate = Color(1.0, 1.0, 1.0, 1.0 if active or current > 0.0 else 0.45) + + +func _update_charge_bar_flash(delta: float) -> void: + if not charge_bar_ready: + charge_flash = 0.0 + return + charge_flash = fmod(charge_flash + delta * 7.0, TAU) + var alpha := 0.62 + 0.38 * absf(sin(charge_flash)) + charge_bar.modulate = Color(1.0, 1.0, 1.0, alpha) + + +func _play_combo_clear_animation() -> void: + if combo_clear_tween != null and combo_clear_tween.is_valid(): + combo_clear_tween.kill() + combo_clear_flash = 1.0 + for panel: PanelContainer in combo_slot_panels: + panel.scale = Vector2(1.16, 1.16) + panel.modulate = Color(1.0, 1.0, 1.0, 1.0) + + +func _update_combo_clear_animation(delta: float) -> void: + if combo_clear_flash <= 0.0: + return + combo_clear_flash = maxf(0.0, combo_clear_flash - delta * 5.0) + var eased := combo_clear_flash * combo_clear_flash + for panel: PanelContainer in combo_slot_panels: + panel.scale = Vector2.ONE * (1.0 + 0.16 * eased) + panel.modulate = Color(1.0, 1.0, 1.0, 0.48 + 0.52 * eased) + if combo_clear_flash <= 0.0: + _restore_empty_combo_slots() + + +func _pulse_combo_slot(panel: PanelContainer) -> void: + var tween := create_tween() + panel.scale = Vector2(1.08, 1.08) + tween.tween_property(panel, "scale", Vector2.ONE, 0.09) + + +func _restore_empty_combo_slots() -> void: + for index: int in range(combo_slot_panels.size()): + combo_slot_panels[index].modulate = Color(1.0, 1.0, 1.0, 0.48) + combo_slot_panels[index].scale = Vector2.ONE + combo_key_labels[index].text = "·" + combo_key_labels[index].modulate = Color(1.0, 1.0, 1.0, 0.32) + + func _update_rhythm_track(delta: float) -> void: beat_flash = maxf(0.0, beat_flash - delta * 8.0) var progress := 0.0 @@ -88,16 +228,110 @@ func _set_control_center(control: Control, center: Vector2, size: Vector2) -> vo func _format_action_name(action_name: String) -> String: match action_name: - "left": - return "LEFT" - "right": - return "RIGHT" - "jump": - return "JUMP" + "w": + return "W" + "a": + return "A" + "d": + return "D" + "s": + return "S" + "space": + return "SP" + "skill_w": + return "W" + "skill_wa": + return "W+A" + "skill_wd": + return "W+D" + "skill_s": + return "S" + "skill_a": + return "A" + "skill_d": + return "D" + "skill_aa": + return "A+A" + "skill_dd": + return "D+D" + "skill_aaa": + return "A+A+A" + "skill_ddd": + return "D+D+D" + "skill_a_space": + return "A+SP" + "skill_d_space": + return "D+SP" + "skill_a_space_space": + return "A+SP+SP" + "skill_d_space_space": + return "D+SP+SP" + "skill_aa_space": + return "A+A+SP" + "skill_ad_space": + return "A+D+SP" + "skill_da_space": + return "D+A+SP" + "skill_dd_space": + return "D+D+SP" + "skill_s_projectile_1": + return "S+SP" + "skill_s_projectile_2": + return "S+SP+SP" + "skill_s_projectile_3": + return "S+SP+SP+SP" _: return action_name.to_upper() +func _format_skill_name(skill_id: String) -> String: + match skill_id: + "skill_w": + return "W" + "skill_wa": + return "W+A" + "skill_wd": + return "W+D" + "skill_s": + return "S" + "skill_a": + return "A" + "skill_d": + return "D" + "skill_aa": + return "A+A" + "skill_dd": + return "D+D" + "skill_aaa": + return "A+A+A" + "skill_ddd": + return "D+D+D" + "skill_a_space": + return "A+SP" + "skill_d_space": + return "D+SP" + "skill_a_space_space": + return "A+SP+SP" + "skill_d_space_space": + return "D+SP+SP" + "skill_aa_space": + return "A+A+SP" + "skill_ad_space": + return "A+D+SP" + "skill_da_space": + return "D+A+SP" + "skill_dd_space": + return "D+D+SP" + "skill_s_projectile_1": + return "S+SP" + "skill_s_projectile_2": + return "S+SP+SP" + "skill_s_projectile_3": + return "S+SP+SP+SP" + _: + return skill_id.to_upper() + + func _format_signed_ms(seconds: float) -> String: if is_inf(seconds): return "-- ms" diff --git a/scenes/main/main.tscn b/scenes/main/main.tscn index 142b9a2..930c335 100644 --- a/scenes/main/main.tscn +++ b/scenes/main/main.tscn @@ -1,7 +1,7 @@ [gd_scene format=3 uid="uid://brx0c2va3831p"] [ext_resource type="PackedScene" uid="uid://cs0rhloanh2u4" path="res://scenes/ground/ground.tscn" id="1_ground"] -[ext_resource type="PackedScene" path="res://scenes/characters/player.tscn" id="2_player"] +[ext_resource type="PackedScene" uid="uid://cs3s5wy1melul" path="res://scenes/characters/player.tscn" id="2_player"] [ext_resource type="Script" uid="uid://3n4nkejauoim" path="res://scenes/main/main.gd" id="3_main_script"] [ext_resource type="Script" uid="uid://brh83qp8flq5u" path="res://scenes/rhythm/rhythm_conductor.gd" id="4_rhythm_script"] [ext_resource type="AudioStream" uid="uid://di5ceecn088rk" path="res://assets/audio/song.ogg" id="5_song"] @@ -11,6 +11,172 @@ [ext_resource type="Texture2D" uid="uid://dbmdivnpjf48l" path="res://assets/ui/rhythm/blue_ball.png" id="9_blue_ball"] [ext_resource type="Texture2D" uid="uid://ewr8k3lwpcna" path="res://assets/ui/rhythm/yellow_ball.png" id="10_yellow_ball"] +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_a8run"] +content_margin_left = 6.0 +content_margin_top = 4.0 +content_margin_right = 6.0 +content_margin_bottom = 4.0 +bg_color = Color(0.04, 0.07, 0.09, 0.82) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.43, 0.78, 0.88, 0.95) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ouso4"] +content_margin_left = 6.0 +content_margin_top = 4.0 +content_margin_right = 6.0 +content_margin_bottom = 4.0 +bg_color = Color(0.04, 0.07, 0.09, 0.82) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.43, 0.78, 0.88, 0.95) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_blune"] +content_margin_left = 6.0 +content_margin_top = 4.0 +content_margin_right = 6.0 +content_margin_bottom = 4.0 +bg_color = Color(0.04, 0.07, 0.09, 0.82) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.43, 0.78, 0.88, 0.95) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_th5th"] +content_margin_left = 6.0 +content_margin_top = 4.0 +content_margin_right = 6.0 +content_margin_bottom = 4.0 +bg_color = Color(0.04, 0.07, 0.09, 0.82) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.43, 0.78, 0.88, 0.95) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7smn1"] +bg_color = Color(0.12, 0.08, 0.08, 0.86) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.6, 0.12, 0.16, 0.95) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_raeie"] +bg_color = Color(0.86, 0.11, 0.18, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hxu8e"] +bg_color = Color(0.18, 0.66, 0.95, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.66, 0.92, 1, 0.9) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nvumn"] +bg_color = Color(0.18, 0.66, 0.95, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.66, 0.92, 1, 0.9) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ou6is"] +bg_color = Color(0.18, 0.66, 0.95, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.66, 0.92, 1, 0.9) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_necax"] +bg_color = Color(0.18, 0.66, 0.95, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.66, 0.92, 1, 0.9) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_r4lks"] +bg_color = Color(0.18, 0.66, 0.95, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.66, 0.92, 1, 0.9) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_pg34l"] +bg_color = Color(0.18, 0.66, 0.95, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.66, 0.92, 1, 0.9) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_m4h2d"] +bg_color = Color(0.18, 0.66, 0.95, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.66, 0.92, 1, 0.9) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_p8abn"] +bg_color = Color(0.18, 0.66, 0.95, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.66, 0.92, 1, 0.9) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_s17dp"] +bg_color = Color(0.18, 0.66, 0.95, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.66, 0.92, 1, 0.9) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_37hlw"] +bg_color = Color(0.18, 0.66, 0.95, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.66, 0.92, 1, 0.9) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_charge_bg"] +bg_color = Color(0.08, 0.07, 0.12, 0.86) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.42, 0.36, 0.75, 0.9) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_charge_fill"] +bg_color = Color(0.92, 0.72, 0.25, 1) + [node name="Main" type="Node2D" unique_id=596674982] script = ExtResource("3_main_script") @@ -190,3 +356,195 @@ theme_override_font_sizes/font_size = 24 text = "READY" horizontal_alignment = 1 vertical_alignment = 1 + +[node name="ComboWindow" type="HBoxContainer" parent="RhythmFeedback" unique_id=1940360666] +anchors_preset = 5 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -148.0 +offset_top = 222.0 +offset_right = 148.0 +offset_bottom = 282.0 +pivot_offset = Vector2(148, 30) +theme_override_constants/separation = 10 + +[node name="Slot0" type="PanelContainer" parent="RhythmFeedback/ComboWindow" unique_id=181099068] +modulate = Color(1, 1, 1, 0.45) +custom_minimum_size = Vector2(64, 56) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_a8run") + +[node name="Key" type="Label" parent="RhythmFeedback/ComboWindow/Slot0" unique_id=1605416584] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_colors/font_color = Color(0.94, 0.98, 1, 1) +theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.9) +theme_override_constants/shadow_offset_x = 2 +theme_override_constants/shadow_offset_y = 2 +theme_override_font_sizes/font_size = 26 +text = "·" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="Slot1" type="PanelContainer" parent="RhythmFeedback/ComboWindow" unique_id=1398681506] +modulate = Color(1, 1, 1, 0.45) +custom_minimum_size = Vector2(64, 56) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_ouso4") + +[node name="Key" type="Label" parent="RhythmFeedback/ComboWindow/Slot1" unique_id=1841250488] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_colors/font_color = Color(0.94, 0.98, 1, 1) +theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.9) +theme_override_constants/shadow_offset_x = 2 +theme_override_constants/shadow_offset_y = 2 +theme_override_font_sizes/font_size = 26 +text = "·" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="Slot2" type="PanelContainer" parent="RhythmFeedback/ComboWindow" unique_id=22762864] +modulate = Color(1, 1, 1, 0.45) +custom_minimum_size = Vector2(64, 56) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_blune") + +[node name="Key" type="Label" parent="RhythmFeedback/ComboWindow/Slot2" unique_id=470444619] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_colors/font_color = Color(0.94, 0.98, 1, 1) +theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.9) +theme_override_constants/shadow_offset_x = 2 +theme_override_constants/shadow_offset_y = 2 +theme_override_font_sizes/font_size = 26 +text = "·" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="Slot3" type="PanelContainer" parent="RhythmFeedback/ComboWindow" unique_id=669931458] +modulate = Color(1, 1, 1, 0.45) +custom_minimum_size = Vector2(64, 56) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_th5th") + +[node name="Key" type="Label" parent="RhythmFeedback/ComboWindow/Slot3" unique_id=1939775423] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_colors/font_color = Color(0.94, 0.98, 1, 1) +theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.9) +theme_override_constants/shadow_offset_x = 2 +theme_override_constants/shadow_offset_y = 2 +theme_override_font_sizes/font_size = 26 +text = "·" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="ComboSkillLabel" type="Label" parent="RhythmFeedback" unique_id=246366341] +anchors_preset = 5 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -240.0 +offset_top = 286.0 +offset_right = 240.0 +offset_bottom = 322.0 +theme_override_colors/font_color = Color(1, 0.84, 0.26, 1) +theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.85) +theme_override_constants/shadow_offset_x = 2 +theme_override_constants/shadow_offset_y = 2 +theme_override_font_sizes/font_size = 18 +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="StatusBars" type="VBoxContainer" parent="RhythmFeedback" unique_id=1850079775] +offset_left = 24.0 +offset_top = 9.0 +offset_right = 294.0 +offset_bottom = 69.0 +theme_override_constants/separation = 8 + +[node name="HealthBar" type="ProgressBar" parent="RhythmFeedback/StatusBars" unique_id=562194184] +custom_minimum_size = Vector2(270, 18) +layout_mode = 2 +theme_override_styles/background = SubResource("StyleBoxFlat_7smn1") +theme_override_styles/fill = SubResource("StyleBoxFlat_raeie") +value = 100.0 +show_percentage = false + +[node name="EnergyBar" type="HBoxContainer" parent="RhythmFeedback/StatusBars" unique_id=353280285] +custom_minimum_size = Vector2(270, 16) +layout_mode = 2 +theme_override_constants/separation = 4 + +[node name="Segment0" type="Panel" parent="RhythmFeedback/StatusBars/EnergyBar" unique_id=1721101704] +modulate = Color(1, 1, 1, 0.38) +custom_minimum_size = Vector2(23, 16) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_hxu8e") + +[node name="Segment1" type="Panel" parent="RhythmFeedback/StatusBars/EnergyBar" unique_id=2071238510] +modulate = Color(1, 1, 1, 0.38) +custom_minimum_size = Vector2(23, 16) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_nvumn") + +[node name="Segment2" type="Panel" parent="RhythmFeedback/StatusBars/EnergyBar" unique_id=820288176] +modulate = Color(1, 1, 1, 0.38) +custom_minimum_size = Vector2(23, 16) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_ou6is") + +[node name="Segment3" type="Panel" parent="RhythmFeedback/StatusBars/EnergyBar" unique_id=1809879636] +modulate = Color(1, 1, 1, 0.38) +custom_minimum_size = Vector2(23, 16) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_necax") + +[node name="Segment4" type="Panel" parent="RhythmFeedback/StatusBars/EnergyBar" unique_id=205364545] +modulate = Color(1, 1, 1, 0.38) +custom_minimum_size = Vector2(23, 16) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_r4lks") + +[node name="Segment5" type="Panel" parent="RhythmFeedback/StatusBars/EnergyBar" unique_id=1414251865] +modulate = Color(1, 1, 1, 0.38) +custom_minimum_size = Vector2(23, 16) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_pg34l") + +[node name="Segment6" type="Panel" parent="RhythmFeedback/StatusBars/EnergyBar" unique_id=1626363537] +modulate = Color(1, 1, 1, 0.38) +custom_minimum_size = Vector2(23, 16) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_m4h2d") + +[node name="Segment7" type="Panel" parent="RhythmFeedback/StatusBars/EnergyBar" unique_id=1577127808] +modulate = Color(1, 1, 1, 0.38) +custom_minimum_size = Vector2(23, 16) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_p8abn") + +[node name="Segment8" type="Panel" parent="RhythmFeedback/StatusBars/EnergyBar" unique_id=1597873707] +modulate = Color(1, 1, 1, 0.38) +custom_minimum_size = Vector2(23, 16) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_s17dp") + +[node name="Segment9" type="Panel" parent="RhythmFeedback/StatusBars/EnergyBar" unique_id=1260417702] +modulate = Color(1, 1, 1, 0.38) +custom_minimum_size = Vector2(23, 16) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_37hlw") + +[node name="ChargeBar" type="ProgressBar" parent="RhythmFeedback/StatusBars" unique_id=674131167] +modulate = Color(1, 1, 1, 0.45) +custom_minimum_size = Vector2(270, 10) +layout_mode = 2 +theme_override_styles/background = SubResource("StyleBoxFlat_charge_bg") +theme_override_styles/fill = SubResource("StyleBoxFlat_charge_fill") +max_value = 1.1 +show_percentage = false diff --git a/tests/test_combo_hud.gd b/tests/test_combo_hud.gd new file mode 100644 index 0000000..133d32c --- /dev/null +++ b/tests/test_combo_hud.gd @@ -0,0 +1,87 @@ +extends SceneTree + +var failures: Array[String] = [] + + +func _init() -> void: + var scene: PackedScene = load("res://scenes/main/main.tscn") + if scene == null: + push_error("Could not load main.tscn") + quit(1) + return + + var main: Node = scene.instantiate() + get_root().add_child(main) + var player: Node = main.get_node_or_null("Player") + if player == null: + failures.append("Missing Player") + elif not player.has_signal("combo_window_cleared"): + failures.append("Player should expose combo_window_cleared") + + if not main.has_method("_play_combo_clear_animation"): + failures.append("Main should implement _play_combo_clear_animation") + if not main.has_method("_on_energy_changed"): + failures.append("Main should implement _on_energy_changed") + if not main.has_method("_on_health_changed"): + failures.append("Main should implement _on_health_changed") + if not main.has_method("_on_charge_changed"): + failures.append("Main should implement _on_charge_changed") + + var status_bars: Node = main.get_node_or_null("RhythmFeedback/StatusBars") + if status_bars == null: + failures.append("Missing StatusBars") + else: + var health_bar := status_bars.get_node_or_null("HealthBar") + if health_bar == null: + failures.append("Missing HealthBar") + elif not health_bar is ProgressBar: + failures.append("HealthBar should be a ProgressBar") + var energy_bar := status_bars.get_node_or_null("EnergyBar") + if energy_bar == null: + failures.append("Missing EnergyBar") + else: + for index: int in range(10): + var segment := energy_bar.get_node_or_null("Segment%d" % index) + if segment == null: + failures.append("Missing energy segment %d" % index) + elif not segment is Panel: + failures.append("Energy segment %d should be a Panel" % index) + var charge_bar := status_bars.get_node_or_null("ChargeBar") + if charge_bar == null: + failures.append("Missing ChargeBar") + elif not charge_bar is ProgressBar: + failures.append("ChargeBar should be a ProgressBar") + elif main.has_method("_on_charge_changed") and main.has_method("_update_charge_bar_flash"): + main.set("charge_bar", charge_bar) + main.call("_on_charge_changed", 1.1, 1.1, true, true) + main.call("_update_charge_bar_flash", 0.13) + var flashing_alpha: float = charge_bar.modulate.a + main.call("_on_charge_changed", 1.1, 1.1, true, true) + if is_equal_approx(charge_bar.modulate.a, 1.0): + failures.append("Ready charge updates should not reset ChargeBar flash alpha") + if not is_equal_approx(charge_bar.modulate.a, flashing_alpha): + failures.append("Ready charge updates should preserve ChargeBar flash alpha") + + var combo_window: Node = main.get_node_or_null("RhythmFeedback/ComboWindow") + if combo_window == null: + failures.append("Missing ComboWindow") + else: + for index: int in range(4): + var slot := combo_window.get_node_or_null("Slot%d" % index) + if slot == null: + failures.append("Missing visual slot %d" % index) + continue + if not slot is PanelContainer: + failures.append("Slot%d should be a PanelContainer" % index) + if slot.get_node_or_null("Key") == null: + failures.append("Slot%d should contain Key label" % index) + + main.free() + + if failures.is_empty(): + print("PASS combo hud") + quit(0) + else: + for failure: String in failures: + push_error(failure) + quit(1) diff --git a/tests/test_combo_hud.gd.uid b/tests/test_combo_hud.gd.uid new file mode 100644 index 0000000..25fedde --- /dev/null +++ b/tests/test_combo_hud.gd.uid @@ -0,0 +1 @@ +uid://sp5kvov8rll3 diff --git a/tests/test_combo_window.gd b/tests/test_combo_window.gd new file mode 100644 index 0000000..ed46a03 --- /dev/null +++ b/tests/test_combo_window.gd @@ -0,0 +1,209 @@ +extends SceneTree + +var failures: Array[String] = [] + + +func _init() -> void: + var window_script: Script = load("res://scenes/combat/combo_window.gd") + var resolver_script: Script = load("res://scenes/combat/input_resolver.gd") + if window_script == null: + failures.append("Missing combo_window.gd") + _finish() + return + if resolver_script == null: + failures.append("Missing input_resolver.gd") + _finish() + return + + var window: RefCounted = window_script.new() + window.record("A") + window.record("Ø") + window.record("SP") + _expect_array(window.get_slots(), ["A", "Ø", "SP"], "miss placeholder should be visible in slots") + _expect_string(window.get_pattern(), "ASP", "miss placeholder should be ignored by pattern") + window.clear("test-reset") + + window.record("W") + window.record("A") + window.record("S") + _expect_array(window.get_slots(), ["W", "A", "S"], "three recorded slots") + window.record("SP") + _expect_array(window.get_slots(), ["W", "A", "S", "SP"], "fourth input should be visible before clear") + _expect_string(window.consume_pending_clear_reason(), "full", "fourth input should request full clear") + window.clear("test-reset") + + window.record("W") + var resolved: Dictionary = resolver_script.resolve(window) + _expect_bool(resolved.is_empty(), true, "W alone should not resolve a skill") + window.record("A") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_wa", "W+A skill") + _expect_array(window.get_slots(), ["W", "A"], "W+A should be visible before skill clear") + window.clear("test-reset") + + window.record("W") + window.record("Ø") + window.record("A") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_a", "miss should break W+A into trailing A only") + window.clear("test-reset") + + window.record("W") + resolved = resolver_script.resolve(window) + _expect_bool(resolved.is_empty(), true, "W alone should not resolve before mirrored W+D") + window.record("D") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_wd", "W+D should mirror W+A skill") + _expect_string(str(resolved.get("animation", "")), "warrior_wa", "W+D should reuse W+A animation") + _expect_string(str(resolved.get("displacement", "")), "right", "W+D should move right") + _expect_array(window.get_slots(), ["W", "D"], "W+D should be visible before skill clear") + window.clear("test-reset") + + window.record("A") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_a", "A skill") + _expect_string(str(resolved.get("animation", "")), "warrior_a", "A should play row 10 animation") + _expect_string(str(resolved.get("displacement", "")), "left", "A should move left") + _expect_bool(bool(resolved.get("clear_window", true)), false, "A skill should not clear window") + window.record("A") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_aa", "A+A skill") + _expect_bool(bool(resolved.get("clear_window", true)), false, "A+A skill should not clear window") + window.record("A") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_aaa", "A+A+A skill") + _expect_bool(bool(resolved.get("clear_window", true)), false, "A+A+A skill should not clear window") + _expect_array(window.get_slots(), ["A", "A", "A"], "A+A+A should be visible before skill clear") + window.clear("test-reset") + + window.record("D") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_d", "D skill") + _expect_string(str(resolved.get("animation", "")), "warrior_a", "D should reuse row 10 animation") + _expect_string(str(resolved.get("displacement", "")), "right", "D should move right") + _expect_bool(bool(resolved.get("clear_window", true)), false, "D skill should not clear window") + window.record("D") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_dd", "D+D should mirror A+A skill") + _expect_string(str(resolved.get("animation", "")), "warrior_aa", "D+D should reuse A+A animation") + window.record("D") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_ddd", "D+D+D should mirror A+A+A skill") + _expect_string(str(resolved.get("animation", "")), "warrior_aaa", "D+D+D should reuse A+A+A animation") + _expect_array(window.get_slots(), ["D", "D", "D"], "D+D+D should be visible before skill clear") + window.clear("test-reset") + + window.record("A") + window.record("SP") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_a_space", "A+Space skill") + _expect_string(str(resolved.get("animation", "")), "warrior_a_space", "A+Space should play row 17 animation") + _expect_string(str(resolved.get("displacement", "")), "left", "A+Space should move left") + _expect_bool(bool(resolved.get("clear_window", false)), true, "A+Space should clear window") + window.record("SP") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_a_space_space", "A+Space+Space skill") + _expect_string(str(resolved.get("animation", "")), "warrior_a_space_space", "A+Space+Space should play row 15 animation") + _expect_bool(bool(resolved.get("clear_window", false)), true, "A+Space+Space should clear window") + window.clear("test-reset") + + window.record("D") + window.record("SP") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_d_space", "D+Space skill") + _expect_string(str(resolved.get("animation", "")), "warrior_a_space", "D+Space should reuse row 17 animation") + _expect_string(str(resolved.get("displacement", "")), "right", "D+Space should move right") + _expect_bool(bool(resolved.get("clear_window", false)), true, "D+Space should clear window") + window.record("SP") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_d_space_space", "D+Space+Space skill") + _expect_string(str(resolved.get("animation", "")), "warrior_a_space_space", "D+Space+Space should reuse row 15 animation") + _expect_bool(bool(resolved.get("clear_window", false)), true, "D+Space+Space should clear window") + window.clear("test-reset") + + window.record("A") + window.record("A") + window.record("SP") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_aa_space", "A+A+Space skill") + _expect_bool(bool(resolved.get("clear_window", false)), true, "A+A+Space should clear window") + window.clear("test-reset") + + window.record("A") + window.record("D") + window.record("SP") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_ad_space", "A+D+Space skill") + _expect_string(str(resolved.get("displacement", "")), "right", "A+D+Space should move toward the last direction") + _expect_bool(bool(resolved.get("clear_window", false)), true, "A+D+Space should clear window") + window.clear("test-reset") + + window.record("D") + window.record("A") + window.record("SP") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_da_space", "D+A+Space skill") + _expect_string(str(resolved.get("displacement", "")), "left", "D+A+Space should move toward the last direction") + _expect_bool(bool(resolved.get("clear_window", false)), true, "D+A+Space should clear window") + window.clear("test-reset") + + window.record("D") + window.record("D") + window.record("SP") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_dd_space", "D+D+Space skill") + _expect_bool(bool(resolved.get("clear_window", false)), true, "D+D+Space should clear window") + window.clear("test-reset") + + window.record("S") + resolved = resolver_script.resolve(window) + _expect_bool(resolved.is_empty(), true, "S alone should not resolve a skill") + window.record("Ø") + window.record("SP") + resolved = resolver_script.resolve(window) + _expect_bool(resolved.is_empty(), true, "S miss Space should not resolve projectile skill") + window.clear("test-reset") + + window.record("S") + resolved = resolver_script.resolve(window) + _expect_bool(resolved.is_empty(), true, "S alone should not resolve a skill") + window.record("SP") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_s_projectile_1", "S+Space skill") + _expect_bool(bool(resolved.get("clear_window", true)), false, "S+Space skill should not clear window") + window.record("SP") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_s_projectile_2", "S+Space+Space skill") + _expect_bool(bool(resolved.get("clear_window", true)), false, "S+Space+Space skill should not clear window") + window.record("SP") + resolved = resolver_script.resolve(window) + _expect_string(str(resolved.get("id", "")), "skill_s_projectile_3", "S+Space+Space+Space skill") + _expect_bool(bool(resolved.get("clear_window", true)), false, "S+Space+Space+Space skill should not clear window") + _expect_array(window.get_slots(), ["S", "SP", "SP", "SP"], "S projectile chain should fill four slots before clear") + + _finish() + + +func _expect_array(actual: Array, expected: Array, label: String) -> void: + if actual != expected: + failures.append("%s: expected %s, got %s" % [label, expected, actual]) + + +func _expect_string(actual: String, expected: String, label: String) -> void: + if actual != expected: + failures.append("%s: expected %s, got %s" % [label, expected, actual]) + + +func _expect_bool(actual: bool, expected: bool, label: String) -> void: + if actual != expected: + failures.append("%s: expected %s, got %s" % [label, expected, actual]) + + +func _finish() -> void: + if failures.is_empty(): + print("PASS combo window") + quit(0) + else: + for failure: String in failures: + push_error(failure) + quit(1) diff --git a/tests/test_combo_window.gd.uid b/tests/test_combo_window.gd.uid new file mode 100644 index 0000000..ce306f2 --- /dev/null +++ b/tests/test_combo_window.gd.uid @@ -0,0 +1 @@ +uid://bti6vtxunhyq5 diff --git a/tests/test_player_air_attack.gd b/tests/test_player_air_attack.gd new file mode 100644 index 0000000..3c6d597 --- /dev/null +++ b/tests/test_player_air_attack.gd @@ -0,0 +1,172 @@ +extends SceneTree + +const WARRIOR_TEXTURE := "res://assets/art/characters/warrior_man_sheet.png" +const WARRIOR_WOMAN_TEXTURE := "res://assets/art/characters/warrior_woman_sheet.png" +const CHARGE_EFFECT_TEXTURE := "res://assets/art/effects/effect_hp_mp_sheet.png" +const EFFECT_TEXTURE := "res://assets/art/effects/effect_sheet.png" +const WARRIOR_COLUMNS := 16 +const WARRIOR_ROWS := 25 + +var failures: Array[String] = [] + + +func _init() -> void: + var scene: PackedScene = load("res://scenes/characters/player.tscn") + if scene == null: + push_error("Could not load player.tscn") + quit(1) + return + + var player: Node = scene.instantiate() + get_root().add_child(player) + var animation_player: AnimationPlayer = player.get_node("AnimationPlayer") as AnimationPlayer + + _expect_action_has_key("player_w", KEY_W) + _expect_action_has_key("player_a", KEY_A) + _expect_action_has_key("player_d", KEY_D) + _expect_action_has_key("player_s", KEY_S) + _expect_action_has_key("player_space", KEY_SPACE) + + _expect_warrior_animation(animation_player, "warrior_idle", 1, 8) + _expect_warrior_animation(animation_player, "warrior_w", 6, 6) + _expect_warrior_animation(animation_player, "warrior_wa", 7, 5) + _expect_warrior_animation(animation_player, "warrior_s", 9, 10) + _expect_warrior_animation(animation_player, "warrior_a", 10, 7) + _expect_warrior_animation(animation_player, "warrior_aa", 11, 5) + _expect_warrior_animation(animation_player, "warrior_aaa", 12, 8) + _expect_warrior_animation(animation_player, "warrior_s_projectile", 14, 13) + _expect_warrior_animation(animation_player, "warrior_charge_release", 13, 16, WARRIOR_WOMAN_TEXTURE) + _expect_warrior_animation(animation_player, "warrior_a_space_space", 15, 12) + _expect_warrior_animation(animation_player, "warrior_a_space", 17, 10) + _expect_charge_effect(player) + + if animation_player.has_animation("player_punch"): + failures.append("Old player_punch animation should be removed") + if animation_player.has_animation("挥砍"): + failures.append("Old slash animation should be removed") + + player.call("submit_combo_input", "W") + _expect_string(str(player.get("last_requested_skill_id")), "", "W alone should not request a skill") + player.call("submit_combo_input", "A") + _expect_string(str(player.get("last_requested_skill_id")), "skill_wa", "W+A should request row 7 skill") + _expect_string(str(player.get("current_skill_animation")), "warrior_wa", "W+A should play warrior_wa") + + var projectile := PlayerProjectile.new() + get_root().add_child(projectile) + _expect_projectile_animation(projectile) + projectile.queue_free() + + player.queue_free() + _finish() + + +func _expect_action_has_key(action_name: String, key: Key) -> void: + if not InputMap.has_action(action_name): + failures.append("Missing input action: %s" % action_name) + return + for event: InputEvent in InputMap.action_get_events(action_name): + var key_event := event as InputEventKey + if key_event != null and (key_event.keycode == key or key_event.physical_keycode == key): + return + failures.append("Input action %s should be bound to key %s" % [action_name, OS.get_keycode_string(key)]) + + +func _expect_warrior_animation(animation_player: AnimationPlayer, animation_name: String, row: int, expected_frames: int, texture_path := WARRIOR_TEXTURE) -> void: + if not animation_player.has_animation(animation_name): + failures.append("Missing animation: %s" % animation_name) + return + var animation: Animation = animation_player.get_animation(animation_name) + var hframes_found := false + var vframes_found := false + var texture_found := false + var frame_values: Array[int] = [] + for track_index: int in range(animation.get_track_count()): + var track_path := animation.track_get_path(track_index) + if track_path == NodePath("CharacterSprite:texture"): + var texture: Texture2D = animation.track_get_key_value(track_index, 0) + texture_found = texture != null and texture.resource_path == texture_path + elif track_path == NodePath("CharacterSprite:hframes"): + hframes_found = true + var hframes: int = animation.track_get_key_value(track_index, 0) + if hframes != WARRIOR_COLUMNS: + failures.append("%s hframes expected %d, got %d" % [animation_name, WARRIOR_COLUMNS, hframes]) + elif track_path == NodePath("CharacterSprite:vframes"): + vframes_found = true + var vframes: int = animation.track_get_key_value(track_index, 0) + if vframes != WARRIOR_ROWS: + failures.append("%s vframes expected %d, got %d" % [animation_name, WARRIOR_ROWS, vframes]) + elif track_path == NodePath("CharacterSprite:frame"): + for key_index: int in range(animation.track_get_key_count(track_index)): + frame_values.append(animation.track_get_key_value(track_index, key_index)) + if not texture_found: + failures.append("%s should use %s" % [animation_name, texture_path]) + if not hframes_found: + failures.append("Missing hframes track: %s" % animation_name) + if not vframes_found: + failures.append("Missing vframes track: %s" % animation_name) + if frame_values.size() != expected_frames: + failures.append("%s should key %d frames, got %d" % [animation_name, expected_frames, frame_values.size()]) + var first_frame := (row - 1) * WARRIOR_COLUMNS + for index: int in range(frame_values.size()): + var expected := first_frame + index + if frame_values[index] != expected: + failures.append("%s frame %d expected sheet frame %d, got %d" % [ + animation_name, + index, + expected, + frame_values[index], + ]) + + +func _expect_string(actual: String, expected: String, label: String) -> void: + if actual != expected: + failures.append("%s: expected %s, got %s" % [label, expected, actual]) + + +func _expect_projectile_animation(projectile: Node) -> void: + if projectile.get_child_count() == 0: + failures.append("Projectile should create a Sprite2D child") + return + var sprite := projectile.get_child(0) as Sprite2D + if sprite == null: + failures.append("Projectile child should be Sprite2D") + return + if sprite.texture == null or sprite.texture.resource_path != EFFECT_TEXTURE: + failures.append("Projectile should use %s" % EFFECT_TEXTURE) + if sprite.hframes != 6: + failures.append("Projectile hframes expected 6, got %d" % sprite.hframes) + if sprite.vframes != 2: + failures.append("Projectile vframes expected 2, got %d" % sprite.vframes) + projectile.call("_process", 0.0) + if sprite.frame != 0: + failures.append("Projectile first frame expected 0, got %d" % sprite.frame) + projectile.call("_process", 0.18) + if sprite.frame != 3: + failures.append("Projectile should use first row frame 3 after 0.18s, got %d" % sprite.frame) + + +func _expect_charge_effect(player: Node) -> void: + var sprite := player.get_node_or_null("ChargeEffectSprite") as Sprite2D + if sprite == null: + failures.append("Player should include ChargeEffectSprite") + return + if sprite.texture == null or sprite.texture.resource_path != CHARGE_EFFECT_TEXTURE: + failures.append("Charge effect should use %s" % CHARGE_EFFECT_TEXTURE) + if sprite.hframes != 5: + failures.append("Charge effect hframes expected 5, got %d" % sprite.hframes) + if sprite.vframes != 2: + failures.append("Charge effect vframes expected 2, got %d" % sprite.vframes) + if sprite.z_index < 1: + failures.append("Charge effect should draw above the player feet, got z_index %d" % sprite.z_index) + if sprite.visible: + failures.append("Charge effect should start hidden") + + +func _finish() -> void: + if failures.is_empty(): + print("PASS player warrior actions") + quit(0) + else: + for failure: String in failures: + push_error(failure) + quit(1) diff --git a/tests/test_player_air_attack.gd.uid b/tests/test_player_air_attack.gd.uid new file mode 100644 index 0000000..0e3d2c1 --- /dev/null +++ b/tests/test_player_air_attack.gd.uid @@ -0,0 +1 @@ +uid://de8rucje4ky17 diff --git a/tests/test_player_combo_input.gd b/tests/test_player_combo_input.gd new file mode 100644 index 0000000..e8fec61 --- /dev/null +++ b/tests/test_player_combo_input.gd @@ -0,0 +1,510 @@ +extends SceneTree + +var failures: Array[String] = [] +var requested_skills: Array[String] = [] + + +func _init() -> void: + var scene: PackedScene = load("res://scenes/characters/player.tscn") + if scene == null: + push_error("Could not load player.tscn") + quit(1) + return + + var player: Node = scene.instantiate() + get_root().add_child(player) + var animation_player: AnimationPlayer = player.get_node("AnimationPlayer") as AnimationPlayer + var supports_energy := player.has_method("get_energy") and player.has_method("get_max_energy") + var supports_charge := player.has_method("get_charge") and player.has_method("get_max_charge") and player.has_method("is_charge_active") and player.has_method("is_charge_ready") + if player.has_signal("skill_requested"): + player.connect("skill_requested", _on_skill_requested) + else: + failures.append("Player missing skill_requested signal") + if not player.has_signal("charge_changed"): + failures.append("Player should expose charge_changed signal") + if supports_charge: + _expect_zero(player.call("get_charge"), "charge should start empty") + _expect_bool(player.call("is_charge_ready"), false, "charge should not start ready") + else: + failures.append("Player should expose charge getters") + if not player.has_signal("energy_changed"): + failures.append("Player should expose energy_changed signal") + if not player.has_signal("health_changed"): + failures.append("Player should expose health_changed signal") + if supports_energy: + _expect_int(player.call("get_max_energy"), 10, "energy bar should have ten segments") + _expect_int(player.call("get_energy"), 0, "energy should start empty") + else: + failures.append("Player should expose get_energy and get_max_energy") + if player.has_method("get_health") and player.has_method("get_max_health"): + _expect_int(player.call("get_health"), player.call("get_max_health"), "health should start full") + else: + failures.append("Player should expose get_health and get_max_health") + + _expect_action("player_w", KEY_W) + _expect_action("player_a", KEY_A) + _expect_action("player_d", KEY_D) + _expect_action("player_s", KEY_S) + _expect_action("player_space", KEY_SPACE) + + var w_event := InputEventKey.new() + w_event.pressed = true + w_event.physical_keycode = KEY_W + player.call("_input", w_event) + w_event.echo = true + player.call("_input", w_event) + _expect_array(player.call("get_combo_slots"), ["W"], "W key press should enter once and ignore echo repeat") + _expect_no_skill_requested("W alone should not request a skill") + player.get("combo_window").clear("test-reset") + + var a_event := InputEventKey.new() + a_event.pressed = true + a_event.physical_keycode = KEY_A + player.call("_input", a_event) + _expect_array(player.call("get_combo_slots"), ["A"], "A alone should stay visible in the combo window") + _expect_last_skill("skill_a", "A should request row 10 skill") + _expect_string(str(player.get("current_skill_animation")), "warrior_a", "A should play row 10 animation") + _expect_negative((player as CharacterBody2D).velocity.x, "A should lunge left") + player.get("combo_window").clear("test-reset") + requested_skills.clear() + + if supports_charge: + player.call("_input", a_event) + player.set("state", Character.State.IDLE) + player.set("attack_time_left", 0.0) + player.call("_process", 0.2) + _expect_bool(player.call("is_charge_active"), true, "holding A after its animation should enter charge state") + _expect_string(animation_player.current_animation, "warrior_idle", "holding A charge should keep idle animation") + _expect_positive(player.call("get_charge"), "holding A should grow charge") + var charge_effect := player.get_node_or_null("ChargeEffectSprite") as Sprite2D + if charge_effect == null: + failures.append("ChargeEffectSprite missing during A charge test") + else: + _expect_bool(charge_effect.visible, true, "holding A should show charge effect") + requested_skills.clear() + var a_release_event := InputEventKey.new() + a_release_event.pressed = false + a_release_event.physical_keycode = KEY_A + player.call("_input", a_release_event) + _expect_bool(player.call("is_charge_active"), false, "early A release should cancel charge") + _expect_bool(player.call("is_charge_ready"), false, "early A release should not be ready") + _expect_no_skill_requested("early A release should not request charge release skill") + player.get("combo_window").clear("test-reset") + requested_skills.clear() + + player.call("_input", a_event) + player.set("state", Character.State.IDLE) + player.set("attack_time_left", 0.0) + player.call("_process", player.call("get_max_charge") + 0.1) + _expect_bool(player.call("is_charge_ready"), true, "held A should become ready when charge is full") + requested_skills.clear() + player.call("_input", a_release_event) + _expect_last_skill("skill_a_charge_release", "full A release should request charge release skill") + _expect_string(str(player.get("current_skill_animation")), "warrior_charge_release", "full A release should play row 13 animation") + _expect_negative((player as CharacterBody2D).velocity.x, "full A release should lunge left") + _expect_bool(player.call("is_charge_active"), false, "full A release should leave charge state") + player.get("combo_window").clear("test-reset") + requested_skills.clear() + + player.call("_input", a_event) + player.call("_input", a_event) + _expect_array(player.call("get_combo_slots"), ["A", "A"], "two separate A presses should both enter the combo window") + _expect_last_skill("skill_aa", "A+A should request row 11 skill") + player.get("combo_window").clear("test-reset") + requested_skills.clear() + + player.call("_input", a_event) + Input.action_press("player_a") + player.call("handle_input") + Input.action_release("player_a") + _expect_array(player.call("get_combo_slots"), ["A"], "one A key event should not be recorded again by physics polling") + _expect_last_skill("skill_a", "single A key event should still be the last requested skill after physics polling") + player.call("flush_pending_combo_clear") + player.get("combo_window").clear("test-reset") + requested_skills.clear() + + var d_event := InputEventKey.new() + d_event.pressed = true + d_event.physical_keycode = KEY_D + player.call("_input", d_event) + _expect_array(player.call("get_combo_slots"), ["D"], "D key press should enter the combo window") + _expect_last_skill("skill_d", "D should request mirrored row 10 skill") + _expect_string(str(player.get("current_skill_animation")), "warrior_a", "D should reuse row 10 animation") + _expect_positive((player as CharacterBody2D).velocity.x, "D should lunge right") + player.call("flush_pending_combo_clear") + player.get("combo_window").clear("test-reset") + requested_skills.clear() + + if supports_charge: + player.call("_input", d_event) + player.set("state", Character.State.IDLE) + player.set("attack_time_left", 0.0) + player.call("_process", player.call("get_max_charge") + 0.1) + var d_release_event := InputEventKey.new() + d_release_event.pressed = false + d_release_event.physical_keycode = KEY_D + requested_skills.clear() + player.call("_input", d_release_event) + _expect_last_skill("skill_d_charge_release", "full D release should request charge release skill") + _expect_string(str(player.get("current_skill_animation")), "warrior_charge_release", "full D release should reuse row 13 animation") + _expect_positive((player as CharacterBody2D).velocity.x, "full D release should lunge right") + player.get("combo_window").clear("test-reset") + requested_skills.clear() + + Input.action_press("player_a") + player.set("state", Character.State.IDLE) + player.set("velocity", Vector2.ZERO) + player.call("handle_input") + _expect_negative((player as CharacterBody2D).velocity.x, "A should move the player left") + _expect_vector(player.get("heading"), Vector2.LEFT, "A should face left") + _expect_array(player.call("get_combo_slots"), [], "physics-only movement polling should not write combo slots") + Input.action_release("player_a") + player.call("flush_pending_combo_clear") + player.get("combo_window").clear("test-reset") + + Input.action_press("player_d") + player.set("state", Character.State.IDLE) + player.set("velocity", Vector2.ZERO) + player.call("handle_input") + _expect_positive((player as CharacterBody2D).velocity.x, "D should move the player right") + _expect_vector(player.get("heading"), Vector2.RIGHT, "D should face right") + Input.action_release("player_d") + player.get("combo_window").clear("test-reset") + + var unhandled_s_event := InputEventKey.new() + unhandled_s_event.pressed = true + unhandled_s_event.physical_keycode = KEY_S + player.call("_unhandled_input", unhandled_s_event) + _expect_array(player.call("get_combo_slots"), ["S"], "unhandled S should enter S") + _expect_no_skill_requested("S alone should not request a skill") + player.get("combo_window").clear("test-reset") + + player.call("submit_combo_input", "S", "miss") + _expect_array(player.call("get_combo_slots"), ["Ø"], "miss should display Ø in the combo window") + player.get("combo_window").clear("test-reset") + + if supports_energy: + player.set("current_energy", 0) + player.call("submit_combo_input", "W", "perfect") + _expect_int(player.call("get_energy"), 2, "perfect input should add two energy segments") + player.get("combo_window").clear("test-reset") + player.call("submit_combo_input", "A", "good") + _expect_int(player.call("get_energy"), 3, "good input should add one energy segment") + player.get("combo_window").clear("test-reset") + player.call("submit_combo_input", "D", "bad") + _expect_int(player.call("get_energy"), 3, "bad input should not add energy") + player.get("combo_window").clear("test-reset") + player.call("submit_combo_input", "S", "miss") + _expect_int(player.call("get_energy"), 3, "miss input should not add energy") + player.get("combo_window").clear("test-reset") + player.set("current_energy", 9) + player.call("submit_combo_input", "W", "perfect") + _expect_int(player.call("get_energy"), 10, "energy should cap at ten segments") + player.get("combo_window").clear("test-reset") + + requested_skills.clear() + player.call("_play_skill_animation", "warrior_a", Vector2.LEFT) + player.call("submit_combo_input", "A", "miss") + _expect_array(player.call("get_combo_slots"), ["Ø"], "missed A should display Ø in the combo window") + _expect_no_skill_requested("missed A should not request a skill") + _expect_zero((player as CharacterBody2D).velocity.x, "missed A should stop horizontal lunge") + _expect_int(int(player.get("state")), Character.State.IDLE, "missed A should return to idle state") + _expect_string(animation_player.current_animation, "warrior_idle", "missed A should keep idle animation") + player.get("combo_window").clear("test-reset") + + requested_skills.clear() + player.call("_play_skill_animation", "warrior_a", Vector2.RIGHT) + player.call("submit_combo_input", "D", "miss") + _expect_array(player.call("get_combo_slots"), ["Ø"], "missed D should display Ø in the combo window") + _expect_no_skill_requested("missed D should not request a skill") + _expect_zero((player as CharacterBody2D).velocity.x, "missed D should stop horizontal lunge") + _expect_int(int(player.get("state")), Character.State.IDLE, "missed D should return to idle state") + _expect_string(animation_player.current_animation, "warrior_idle", "missed D should keep idle animation") + player.get("combo_window").clear("test-reset") + + player.call("submit_combo_input", "W", "perfect") + player.call("submit_combo_input", "A", "good") + _expect_array(player.call("get_combo_slots"), ["W", "A"], "W+A should stay visible after skill trigger") + _expect_last_skill("skill_wa", "W+A should request row 7 skill") + _expect_string(str(player.get("current_skill_animation")), "warrior_wa", "W+A should play row 7 animation") + _expect_negative((player as CharacterBody2D).velocity.x, "W+A should lunge left") + player.call("flush_pending_combo_clear") + _expect_array(player.call("get_combo_slots"), ["W", "A"], "W+A should not clear combo window") + player.get("combo_window").clear("test-reset") + + player.call("submit_combo_input", "W", "perfect") + player.call("submit_combo_input", "D", "good") + _expect_array(player.call("get_combo_slots"), ["W", "D"], "W+D should stay visible after skill trigger") + _expect_last_skill("skill_wd", "W+D should request mirrored row 7 skill") + _expect_string(str(player.get("current_skill_animation")), "warrior_wa", "W+D should reuse row 7 animation") + _expect_positive((player as CharacterBody2D).velocity.x, "W+D should lunge right") + _expect_vector(player.get("heading"), Vector2.RIGHT, "W+D should face right") + player.call("flush_pending_combo_clear") + _expect_array(player.call("get_combo_slots"), ["W", "D"], "W+D should not clear combo window") + player.get("combo_window").clear("test-reset") + + player.call("submit_combo_input", "A") + player.call("submit_combo_input", "A") + player.call("submit_combo_input", "A") + _expect_array(player.call("get_combo_slots"), ["A", "A", "A"], "A+A+A should stay visible after skill trigger") + _expect_last_skill("skill_aaa", "A+A+A should request row 12 skill") + _expect_string(str(player.get("current_skill_animation")), "warrior_aaa", "A+A+A should play row 12 animation") + _expect_negative((player as CharacterBody2D).velocity.x, "A+A+A should lunge left") + player.call("flush_pending_combo_clear") + _expect_array(player.call("get_combo_slots"), ["A", "A", "A"], "A+A+A should not clear combo window") + player.get("combo_window").clear("test-reset") + + player.call("submit_combo_input", "A") + player.call("submit_combo_input", "A") + player.call("submit_combo_input", "A") + player.call("submit_combo_input", "A") + _expect_array(player.call("get_combo_slots"), ["A", "A", "A", "A"], "fourth A should still fill the old four-slot window before clear") + _expect_last_skill("skill_a", "fourth A after A+A+A should play normal A animation") + _expect_string(str(player.get("current_skill_animation")), "warrior_a", "fourth A should fall back to row 10 animation") + _expect_negative((player as CharacterBody2D).velocity.x, "fourth A should lunge left as a normal A") + player.call("flush_pending_combo_clear") + _expect_array(player.call("get_combo_slots"), [], "fourth A full window should clear after display") + player.get("combo_window").clear("test-reset") + + player.call("submit_combo_input", "D") + player.call("submit_combo_input", "D") + player.call("submit_combo_input", "D") + _expect_array(player.call("get_combo_slots"), ["D", "D", "D"], "D+D+D should stay visible after skill trigger") + _expect_last_skill("skill_ddd", "D+D+D should request mirrored row 12 skill") + _expect_string(str(player.get("current_skill_animation")), "warrior_aaa", "D+D+D should reuse row 12 animation") + _expect_positive((player as CharacterBody2D).velocity.x, "D+D+D should lunge right") + _expect_vector(player.get("heading"), Vector2.RIGHT, "D+D+D should face right") + player.call("flush_pending_combo_clear") + _expect_array(player.call("get_combo_slots"), ["D", "D", "D"], "D+D+D should not clear combo window") + player.get("combo_window").clear("test-reset") + + player.call("submit_combo_input", "D") + player.call("submit_combo_input", "D") + player.call("submit_combo_input", "D") + player.call("submit_combo_input", "D") + _expect_array(player.call("get_combo_slots"), ["D", "D", "D", "D"], "fourth D should still fill the old four-slot window before clear") + _expect_last_skill("skill_d", "fourth D after D+D+D should play normal D animation") + _expect_string(str(player.get("current_skill_animation")), "warrior_a", "fourth D should fall back to row 10 animation") + _expect_positive((player as CharacterBody2D).velocity.x, "fourth D should lunge right as a normal D") + player.call("flush_pending_combo_clear") + _expect_array(player.call("get_combo_slots"), [], "fourth D full window should clear after display") + player.get("combo_window").clear("test-reset") + + player.call("submit_combo_input", "A") + player.call("submit_combo_input", "SP") + _expect_array(player.call("get_combo_slots"), ["A", "SP"], "A+Space should be visible before skill clear") + _expect_last_skill("skill_a_space", "A+Space should request row 17 skill") + _expect_string(str(player.get("current_skill_animation")), "warrior_a_space", "A+Space should play row 17 animation") + _expect_negative((player as CharacterBody2D).velocity.x, "A+Space should lunge left") + player.call("flush_pending_combo_clear") + _expect_array(player.call("get_combo_slots"), [], "A+Space should clear combo window") + player.get("combo_window").clear("test-reset") + + player.call("submit_combo_input", "D") + player.call("submit_combo_input", "SP") + _expect_array(player.call("get_combo_slots"), ["D", "SP"], "D+Space should be visible before skill clear") + _expect_last_skill("skill_d_space", "D+Space should request mirrored row 17 skill") + _expect_string(str(player.get("current_skill_animation")), "warrior_a_space", "D+Space should reuse row 17 animation") + _expect_positive((player as CharacterBody2D).velocity.x, "D+Space should lunge right") + player.call("flush_pending_combo_clear") + _expect_array(player.call("get_combo_slots"), [], "D+Space should clear combo window") + player.get("combo_window").clear("test-reset") + + player.call("submit_combo_input", "A") + player.call("submit_combo_input", "SP") + player.call("submit_combo_input", "SP") + _expect_array(player.call("get_combo_slots"), ["A", "SP", "SP"], "A+Space+Space should cancel the pending A+Space clear and stay visible before its own clear") + _expect_last_skill("skill_a_space_space", "A+Space+Space should request row 15 skill") + _expect_string(str(player.get("current_skill_animation")), "warrior_a_space_space", "A+Space+Space should play row 15 animation") + _expect_negative((player as CharacterBody2D).velocity.x, "A+Space+Space should lunge left") + player.call("flush_pending_combo_clear") + _expect_array(player.call("get_combo_slots"), [], "A+Space+Space should clear combo window") + player.get("combo_window").clear("test-reset") + + player.call("submit_combo_input", "D") + player.call("submit_combo_input", "SP") + player.call("submit_combo_input", "SP") + _expect_array(player.call("get_combo_slots"), ["D", "SP", "SP"], "D+Space+Space should cancel the pending D+Space clear and stay visible before its own clear") + _expect_last_skill("skill_d_space_space", "D+Space+Space should request mirrored row 15 skill") + _expect_string(str(player.get("current_skill_animation")), "warrior_a_space_space", "D+Space+Space should reuse row 15 animation") + _expect_positive((player as CharacterBody2D).velocity.x, "D+Space+Space should lunge right") + player.call("flush_pending_combo_clear") + _expect_array(player.call("get_combo_slots"), [], "D+Space+Space should clear combo window") + player.get("combo_window").clear("test-reset") + + player.call("submit_combo_input", "A") + player.call("submit_combo_input", "A") + player.call("submit_combo_input", "SP") + _expect_array(player.call("get_combo_slots"), ["A", "A", "SP"], "A+A+Space should be visible before skill clear") + _expect_last_skill("skill_aa_space", "A+A+Space should request clear skill") + player.call("flush_pending_combo_clear") + _expect_array(player.call("get_combo_slots"), [], "A+A+Space should clear combo window") + player.get("combo_window").clear("test-reset") + + player.call("submit_combo_input", "D") + player.call("submit_combo_input", "D") + player.call("submit_combo_input", "SP") + _expect_array(player.call("get_combo_slots"), ["D", "D", "SP"], "D+D+Space should be visible before skill clear") + _expect_last_skill("skill_dd_space", "D+D+Space should request clear skill") + player.call("flush_pending_combo_clear") + _expect_array(player.call("get_combo_slots"), [], "D+D+Space should clear combo window") + player.get("combo_window").clear("test-reset") + + player.call("submit_combo_input", "SP") + _expect_array(player.call("get_combo_slots"), ["SP"], "Space should be visible before space clear") + player.call("flush_pending_combo_clear") + _expect_array(player.call("get_combo_slots"), [], "Space should clear combo window") + + if supports_energy: + player.set("current_energy", 0) + player.call("submit_combo_input", "S", "perfect") + player.call("submit_combo_input", "SP", "perfect") + _expect_last_skill("skill_s_projectile_1", "S+Space should request projectile skill") + _expect_projectile_count(1, "S+Space should fire one projectile") + if supports_energy: + _expect_int(player.call("get_energy"), 1, "S+Space should spend three energy after two perfect inputs") + player.call("flush_pending_combo_clear") + _expect_array(player.call("get_combo_slots"), ["S", "SP"], "S+Space should not clear combo window") + player.call("submit_combo_input", "SP", "perfect") + _expect_last_skill("skill_s_projectile_2", "S+Space+Space should request projectile skill") + _expect_projectile_count(2, "Second Space should fire another projectile") + if supports_energy: + _expect_int(player.call("get_energy"), 1, "S+Space+Space should spend two energy after the next perfect input") + player.call("flush_pending_combo_clear") + _expect_array(player.call("get_combo_slots"), ["S", "SP", "SP"], "S+Space+Space should not clear combo window") + player.call("submit_combo_input", "SP", "perfect") + _expect_last_skill("skill_s_projectile_3", "S+Space+Space+Space should request projectile skill") + _expect_projectile_count(3, "Third Space should fire another projectile") + if supports_energy: + _expect_int(player.call("get_energy"), 2, "S+Space+Space+Space should spend one energy after the next perfect input") + _expect_array(player.call("get_combo_slots"), ["S", "SP", "SP", "SP"], "projectile chain should fill four slots before clear") + player.call("flush_pending_combo_clear") + _expect_array(player.call("get_combo_slots"), [], "projectile chain should clear combo window because four slots are full") + + if supports_energy: + requested_skills.clear() + player.set("current_energy", 0) + player.get("combo_window").clear("test-reset") + player.call("submit_combo_input", "S", "bad") + player.call("submit_combo_input", "SP", "bad") + _expect_no_skill_requested("S+Space should not execute when energy is insufficient") + _expect_projectile_count(3, "insufficient energy should not fire another projectile") + _expect_int(player.call("get_energy"), 0, "insufficient projectile attempt should leave energy at zero") + player.get("combo_window").clear("test-reset") + + requested_skills.clear() + if supports_energy: + player.set("current_energy", 10) + player.get("combo_window").clear("test-reset") + player.call("submit_combo_input", "S", "perfect") + player.call("submit_combo_input", "A", "miss") + player.call("submit_combo_input", "SP", "perfect") + _expect_array(player.call("get_combo_slots"), ["S", "Ø", "SP"], "miss should remain visible between S and Space") + _expect_no_skill_requested("S miss Space should not execute projectile skill") + _expect_projectile_count(3, "S miss Space should not fire another projectile") + player.call("flush_pending_combo_clear") + _expect_array(player.call("get_combo_slots"), [], "S miss Space should clear as a normal Space input") + player.get("combo_window").clear("test-reset") + + player.call("submit_combo_input", "W") + player.call("submit_combo_input", "W") + player.call("submit_combo_input", "W") + player.call("submit_combo_input", "W") + _expect_array(player.call("get_combo_slots"), ["W", "W", "W", "W"], "four non-skill inputs should be visible before clear") + player.call("flush_pending_combo_clear") + _expect_array(player.call("get_combo_slots"), [], "four non-skill inputs should clear combo window") + + player.queue_free() + _finish() + + +func _expect_action(action_name: String, key: Key) -> void: + if not InputMap.has_action(action_name): + failures.append("Missing input action: %s" % action_name) + return + for event: InputEvent in InputMap.action_get_events(action_name): + var key_event := event as InputEventKey + if key_event != null and (key_event.keycode == key or key_event.physical_keycode == key): + return + failures.append("Input action %s should be bound to %s" % [action_name, OS.get_keycode_string(key)]) + + +func _expect_last_skill(expected: String, label: String) -> void: + if requested_skills.is_empty(): + failures.append("%s: no skill requested" % label) + return + var actual := requested_skills[requested_skills.size() - 1] + if actual != expected: + failures.append("%s: expected %s, got %s" % [label, expected, actual]) + + +func _expect_no_skill_requested(label: String) -> void: + if not requested_skills.is_empty(): + failures.append("%s: expected no skill, got %s" % [label, requested_skills[requested_skills.size() - 1]]) + + +func _expect_string(actual: String, expected: String, label: String) -> void: + if actual != expected: + failures.append("%s: expected %s, got %s" % [label, expected, actual]) + + +func _expect_int(actual: int, expected: int, label: String) -> void: + if actual != expected: + failures.append("%s: expected %d, got %d" % [label, expected, actual]) + + +func _expect_projectile_count(expected: int, label: String) -> void: + var actual := _count_projectiles(get_root()) + if actual != expected: + failures.append("%s: expected %d, got %d" % [label, expected, actual]) + + +func _count_projectiles(node: Node) -> int: + var total := 1 if node.is_in_group("player_projectiles") else 0 + for child: Node in node.get_children(): + total += _count_projectiles(child) + return total + + +func _expect_array(actual: Array, expected: Array, label: String) -> void: + if actual != expected: + failures.append("%s: expected %s, got %s" % [label, expected, actual]) + + +func _expect_negative(actual: float, label: String) -> void: + if actual >= 0.0: + failures.append("%s: expected negative x velocity, got %.3f" % [label, actual]) + + +func _expect_positive(actual: float, label: String) -> void: + if actual <= 0.0: + failures.append("%s: expected positive x velocity, got %.3f" % [label, actual]) + + +func _expect_bool(actual: bool, expected: bool, label: String) -> void: + if actual != expected: + failures.append("%s: expected %s, got %s" % [label, expected, actual]) + + +func _expect_zero(actual: float, label: String) -> void: + if not is_zero_approx(actual): + failures.append("%s: expected zero x velocity, got %.3f" % [label, actual]) + + +func _expect_vector(actual: Vector2, expected: Vector2, label: String) -> void: + if not actual.is_equal_approx(expected): + failures.append("%s: expected %s, got %s" % [label, expected, actual]) + + +func _on_skill_requested(skill_id: String) -> void: + requested_skills.append(skill_id) + + +func _finish() -> void: + if failures.is_empty(): + print("PASS player combo input") + quit(0) + else: + for failure: String in failures: + push_error(failure) + quit(1) diff --git a/tests/test_player_combo_input.gd.uid b/tests/test_player_combo_input.gd.uid new file mode 100644 index 0000000..e6b66ba --- /dev/null +++ b/tests/test_player_combo_input.gd.uid @@ -0,0 +1 @@ +uid://cv8ly1jk7ksoo diff --git a/tests/test_player_scale.gd b/tests/test_player_scale.gd index e1aa373..92a00f4 100644 --- a/tests/test_player_scale.gd +++ b/tests/test_player_scale.gd @@ -1,6 +1,20 @@ extends SceneTree var failures: Array[String] = [] +const PLAYER_SPRITE_ANCHOR := Vector2(-40, -48) +const WARRIOR_ANIMATIONS := [ + "warrior_idle", + "warrior_w", + "warrior_wa", + "warrior_s", + "warrior_a", + "warrior_aa", + "warrior_aaa", + "warrior_charge_release", + "warrior_s_projectile", + "warrior_a_space_space", + "warrior_a_space", +] func _init() -> void: @@ -18,14 +32,13 @@ func _init() -> void: _expect_vector((player as Node2D).scale, Vector2(4, 4), "Player root scale") _expect_vector(sprite.scale, Vector2.ONE, "CharacterSprite local scale should keep anchor") - _expect_vector(sprite.offset, Vector2(-24, -40), "CharacterSprite visible foot offset") + _expect_vector(sprite.offset, PLAYER_SPRITE_ANCHOR, "CharacterSprite visible foot offset") _expect_vector(collision.scale, Vector2.ONE, "CollisionShape2D local scale should keep anchor") _expect_vector(collision.position, Vector2(0, -18), "CollisionShape2D local position should keep anchor") _expect_vector(camera.position, Vector2(0, -37.5), "Camera2D position should compensate player scale") _expect_vector(camera.scale, Vector2(0.25, 0.25), "Camera2D scale should compensate player scale") - _expect_animation_offset(animation_player, "idle", Vector2(-24, -40)) - _expect_animation_offset(animation_player, "jump", Vector2(-24, -44)) - _expect_animation_offset(animation_player, "挥砍", Vector2(-40, -48)) + for animation_name: String in WARRIOR_ANIMATIONS: + _expect_animation_offset(animation_player, animation_name, PLAYER_SPRITE_ANCHOR) player.free()