commit 313bc87210bf03da5134955787ff5600d5b709c6 Author: Gnarwhal Date: Sun Apr 25 17:48:30 2021 -0400 Final product diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..193d059 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea/ +out/ +*.iml + diff --git a/res/illustrator/player.ai b/res/illustrator/player.ai new file mode 100644 index 0000000..21e2758 --- /dev/null +++ b/res/illustrator/player.ai @@ -0,0 +1,1132 @@ +%PDF-1.6 % +1 0 obj <>/OCGs[24 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + player + + + Adobe Illustrator 25.2 (Windows) + 2021-04-25T04:51:18-04:00 + 2021-04-25T04:51:19-04:00 + 2021-04-25T04:51:19-04:00 + + + + 256 + 56 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAOAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8ALs5V9ydirsVdirsVdir0 D8kf+Uyf/mDl/wCJJmd2d/efB5j2t/xQf1x9xe9ZvHzR2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2K uxV2KuxV2KuxV2KuxV2KuxV2KuxV8i5yr7ky1ETgvwjoO2ZToyTa7gn8o+7FFl3BP5R92K2XcE/l H3YrZS3XVUWiUAH7wf8AETleTk5ejPqPuZH+SP8AymT/APMHL/xJMyOzv7z4Oq9rf8UH9cfcXvWb x80dirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVfIucq+5MvT7C/ IZlOiPNvFXYq7FUt17/eNP8AjIP+InK8nJy9H9Z9zIvyR/5TJ/8AmDl/4kmZHZ3958HVe1v+KD+u PuL3rN4+aOxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KvkXOVfc mXp9hfkMynRHm3irsVdiqW69/vGn/GQf8ROV5OTl6P6z7mRfkj/ymT/8wcv/ABJMyOzv7z4Oq9rf 8UH9cfcXvWbx80dirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVfI ucq+5MvT7C/IZlOiPNvFXYq7FUt17/eNP+Mg/wCInK8nJy9H9Z9zIvyR/wCUyf8A5g5f+JJmR2d/ efB1Xtb/AIoP64+4ves3j5o7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq+Rc5V9yT1ddtAoHCTYeC/1y7xA606OXeG/wBPWf8AJJ9y/wBcfEC/k594d+nrP+ST7l/r j4gX8nPvDv09Z/ySfcv9cfEC/k594Qmp6nBdQLHGrBgwb4gKUoR2J8cjOYIbtPp5QlZZb+SP/KZP /wAwcv8AxJMyuzv7z4Ok9rf8UH9cfcXvWbx80dirsVdirsVdirsVdirsVdirsVdirsVdirsVdirs VdirsVdirsVdirsVdirsVeC/8qR85fz2f/I1v+aM0f8AJ2TyfS/9Fuk7p/Ifrd/ypHzl/PZ/8jW/ 5ox/k7J5L/ot0ndP5D9bv+VI+cv57P8A5Gt/zRj/ACdk8l/0W6Tun8h+t3/KkfOX89n/AMjW/wCa Mf5OyeS/6LdJ3T+Q/W7/AJUj5y/ns/8Aka3/ADRj/J2TyX/RbpO6fyH63f8AKkfOX89n/wAjW/5o x/k7J5L/AKLdJ3T+Q/Wyr8t/y38w+XPMLahqDW5gNu8QETszcmZSNiq/y5laTSTxzs1ydL2727g1 WDw8fFxcQO49/m9QzZPHOxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Ku xV2Kv//Z + + + + uuid:C1BCCE1871B8DB11993190FCD52B4E9F + xmp.did:96907d70-99ac-b74f-a604-d6aa9bb2b05b + uuid:63c2a1f7-4a77-4815-bebf-c693e1f74543 + proof:pdf + + uuid:9bca8fc8-9642-4c5f-b0dd-85a18880d3e0 + xmp.did:461cb6e1-8371-3c41-b26d-cfc68518b44c + uuid:C1BCCE1871B8DB11993190FCD52B4E9F + proof:pdf + + + + + saved + xmp.iid:96907d70-99ac-b74f-a604-d6aa9bb2b05b + 2021-04-24T21:00:13-04:00 + Adobe Illustrator 25.2 (Windows) + / + + + + Document + Mobile + AIRobin + 1 + False + False + + 16.000000 + 16.000000 + Pixels + + + + Magenta + Yellow + + + + + + Default Swatch Group + 0 + + + + White + RGB + PROCESS + 255 + 255 + 255 + + + Black + RGB + PROCESS + 0 + 0 + 0 + + + RGB Red + RGB + PROCESS + 255 + 0 + 0 + + + RGB Yellow + RGB + PROCESS + 255 + 255 + 0 + + + RGB Green + RGB + PROCESS + 0 + 255 + 0 + + + RGB Cyan + RGB + PROCESS + 0 + 255 + 255 + + + RGB Blue + RGB + PROCESS + 0 + 0 + 255 + + + RGB Magenta + RGB + PROCESS + 255 + 0 + 255 + + + R=193 G=39 B=45 + RGB + PROCESS + 193 + 39 + 45 + + + R=237 G=28 B=36 + RGB + PROCESS + 237 + 28 + 36 + + + R=241 G=90 B=36 + RGB + PROCESS + 241 + 90 + 36 + + + R=247 G=147 B=30 + RGB + PROCESS + 247 + 147 + 30 + + + R=251 G=176 B=59 + RGB + PROCESS + 251 + 176 + 59 + + + R=252 G=238 B=33 + RGB + PROCESS + 252 + 238 + 33 + + + R=217 G=224 B=33 + RGB + PROCESS + 217 + 224 + 33 + + + R=140 G=198 B=63 + RGB + PROCESS + 140 + 198 + 63 + + + R=57 G=181 B=74 + RGB + PROCESS + 57 + 181 + 74 + + + R=0 G=146 B=69 + RGB + PROCESS + 0 + 146 + 69 + + + R=0 G=104 B=55 + RGB + PROCESS + 0 + 104 + 55 + + + R=34 G=181 B=115 + RGB + PROCESS + 34 + 181 + 115 + + + R=0 G=169 B=157 + RGB + PROCESS + 0 + 169 + 157 + + + R=41 G=171 B=226 + RGB + PROCESS + 41 + 171 + 226 + + + R=0 G=113 B=188 + RGB + PROCESS + 0 + 113 + 188 + + + R=46 G=49 B=146 + RGB + PROCESS + 46 + 49 + 146 + + + R=27 G=20 B=100 + RGB + PROCESS + 27 + 20 + 100 + + + R=102 G=45 B=145 + RGB + PROCESS + 102 + 45 + 145 + + + R=147 G=39 B=143 + RGB + PROCESS + 147 + 39 + 143 + + + R=158 G=0 B=93 + RGB + PROCESS + 158 + 0 + 93 + + + R=212 G=20 B=90 + RGB + PROCESS + 212 + 20 + 90 + + + R=237 G=30 B=121 + RGB + PROCESS + 237 + 30 + 121 + + + R=199 G=178 B=153 + RGB + PROCESS + 199 + 178 + 153 + + + R=153 G=134 B=117 + RGB + PROCESS + 153 + 134 + 117 + + + R=115 G=99 B=87 + RGB + PROCESS + 115 + 99 + 87 + + + R=83 G=71 B=65 + RGB + PROCESS + 83 + 71 + 65 + + + R=198 G=156 B=109 + RGB + PROCESS + 198 + 156 + 109 + + + R=166 G=124 B=82 + RGB + PROCESS + 166 + 124 + 82 + + + R=140 G=98 B=57 + RGB + PROCESS + 140 + 98 + 57 + + + R=117 G=76 B=36 + RGB + PROCESS + 117 + 76 + 36 + + + R=96 G=56 B=19 + RGB + PROCESS + 96 + 56 + 19 + + + R=66 G=33 B=11 + RGB + PROCESS + 66 + 33 + 11 + + + + + + Grays + 1 + + + + R=0 G=0 B=0 + RGB + PROCESS + 0 + 0 + 0 + + + R=26 G=26 B=26 + RGB + PROCESS + 26 + 26 + 26 + + + R=51 G=51 B=51 + RGB + PROCESS + 51 + 51 + 51 + + + R=77 G=77 B=77 + RGB + PROCESS + 77 + 77 + 77 + + + R=102 G=102 B=102 + RGB + PROCESS + 102 + 102 + 102 + + + R=128 G=128 B=128 + RGB + PROCESS + 128 + 128 + 128 + + + R=153 G=153 B=153 + RGB + PROCESS + 153 + 153 + 153 + + + R=179 G=179 B=179 + RGB + PROCESS + 179 + 179 + 179 + + + R=204 G=204 B=204 + RGB + PROCESS + 204 + 204 + 204 + + + R=230 G=230 B=230 + RGB + PROCESS + 230 + 230 + 230 + + + R=242 G=242 B=242 + RGB + PROCESS + 242 + 242 + 242 + + + + + + Mobile Color Group + 1 + + + + R=136 G=168 B=13 + RGB + PROCESS + 136 + 168 + 13 + + + R=127 G=71 B=221 + RGB + PROCESS + 127 + 71 + 221 + + + R=251 G=174 B=23 + RGB + PROCESS + 251 + 174 + 23 + + + + + + + Adobe PDF library 15.00 + + + + + + + + + + + + + + + + + + + + + + + + + +endstream endobj 3 0 obj <> endobj 5 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 16.0 16.0]/Type/Page>> endobj 21 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 16.0 16.0]/Type/Page>> endobj 22 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 16.0 16.0]/Type/Page>> endobj 23 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 16.0 16.0]/Type/Page>> endobj 26 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 16.0 16.0]/Type/Page>> endobj 33 0 obj <>stream +HwVu6PprqVw6PH.V0C<.}wPz1!]ʕeh) 4 >x +endstream endobj 24 0 obj <> endobj 34 0 obj [/View/Design] endobj 35 0 obj <>>> endobj 29 0 obj <> endobj 28 0 obj [/ICCBased 36 0 R] endobj 36 0 obj <>stream +HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  + 2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 +V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= +x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- +ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 +N')].uJr + wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 +n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! +zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km +endstream endobj 7 0 obj <> endobj 16 0 obj <> endobj 17 0 obj <>stream +%!PS-Adobe-3.0 +%%Creator: Adobe Illustrator(R) 24.0 +%%AI8_CreatorVersion: 25.2.1 +%%For: (Gnarwhal) () +%%Title: (player.ai) +%%CreationDate: 4/25/2021 4:51 AM +%%Canvassize: 16383 +%%BoundingBox: 0 -16 77 0 +%%HiResBoundingBox: 0.000000001076842 -16 77 0 +%%DocumentProcessColors: Magenta Yellow +%AI5_FileFormat 14.0 +%AI12_BuildNumber: 236 +%AI3_ColorUsage: Color +%AI7_ImageSettings: 0 +%%RGBProcessColor: 0 0 0 ([Registration]) +%AI3_Cropmarks: 64 -16 80 0 +%AI3_TemplateBox: 8.5 -8.5 8.5 -8.5 +%AI3_TileBox: -222 -392 366 376 +%AI3_DocumentPreview: None +%AI5_ArtSize: 14400 14400 +%AI5_RulerUnits: 6 +%AI24_LargeCanvasScale: 1 +%AI9_ColorModel: 1 +%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 +%AI5_TargetResolution: 800 +%AI5_NumLayers: 1 +%AI9_OpenToView: -137.666666666667 79 6 1936 1274 18 0 0 78 121 0 0 0 1 1 0 1 1 0 0 +%AI5_OpenViewLayers: 7 +%%PageOrigin:-80 -112 +%AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 +%AI9_Flatten: 1 +%AI12_CMSettings: 00.MC +%%EndComments + +endstream endobj 18 0 obj <>stream +%AI24_ZStandard_Data(/Xg)  j9WiBBL;p}V*Ŧ m,!s̋̌4V hi64?4MkC:Qߜ )_+iᲨJ=)Ig/2ZܰNq&ɇY}rqSpJwv6,LUf|I7H?%9r?:"S%/csGevn);dܰ8 )e;MmPY.?bTu7K1eASፔJox4.r'&X3XMru+_ džwf Yy/]X3,N2,2ZS/fqtX~ +[j[B(C3טOqD+S>h:EOȣ#r)]&6TؐJmqxA.x4=,*A.v q o^tQ.*ύ!*JU\>ڧ|:F3r=ŌGTތJAqƼul^hQ#zAJu є+mND,Ht45SE,hd7p DA6v0bacVUßd7+ vuX ")9thHUy_ ɠ@\pE + +>PH8P@*(X,@!'P( ( (#%\lЦ:=xQaT6 ڦ͍*:BDL.H8@Q  &r5f]ՙn米#@xÅ.  ᓸO"ᰐpP0BxlՅB%̄b@C` &wG%x=7PՖ P0I1%Gf)!9H.)ayEU2; ȊW:6(;Έ% &HcJ8B M|'BRCARhhH8\N U'p3 S&L2&*(JXpXx2 p + '̬>C +n\cve_<~3Ic#ڔ QJd);Ctg~!4|W+bs; &N&P! B!p_k5+ !S +I8xA $Az#/$N)PR@fLg)s")tj!YTɇωIbS5"ӛygJ~PV!` # A ,(ЂmRTC#)zoHq!tGlul2|8 \š`B +  pYA.Y%C * F +L pXH(<@ð@($8lhCpXXPC ;ĕBDÄCb0_( +p4$HpP`V +8@ SbHC 'F4NKX`H8CKG|pCCÂ0P{dRмC +Zg,F#3hH!uJV3frEe W + O8BX$pHpP1B;psApHeN.Y &@ +Mt棰0 C#)HFP#3CLp g}t~ Dpp6`!)JW3')\{h"vpXH80H|C!PJ,0$fah꬀H,,0p0BS`DBCC!&W@$.PYA!`*0, i7(4$ K8\& +H8t,P) PB¡lH$IBBF (P +%nG-5,xpXx`Rph!H8gEӄ04,(B C"pB!H īpl7!Ne9&ܢL$ڽ1DN\$"UBϽydxR!R#olLWgұx\H  0S@ yw ia-<ڠ)Hr ++}bXXp8B8 ꎆDƺh2Y=JCCpH$p`*p"PI8t:{b :B!H8,(( AC$q! ]C,,,4,a΀`X`h``4T5ѰАp0J$ CC7p-I,"PSiJ8! +FSZ¡hF!p`PjӕH 0I8 㹩2( >+a!,  Kjd@U, b +64324q] 8, 0Ra!p8+p@Japxqg=2irA :*"" 1Lb㱅*RGl(处 BDX@@ uHQW%wݐΙm(\`ә'HFPX1B00`vf),zcwg%o'`;pQ),(0H8bA&AJXN<@$C!! \,B- &eb8[K(|ggtkAN!a CvάllP p@Jn+jX,)j bZ@t2a Ѐ`@ f8#b0 dBBa _-,tАhH0$ D@$ %j$ P8ˆC\!A!" + + + + +X +` +F +C*(4, ^7, `|6񸮸da jjh4400,0,0$p V @CbAa^[Y,,$'D%(1 &$H GEAaA!PB)B _8olɴxg7k)Y墚b %IfFթơAr1!!N aK05X¡x| +S8K.q p p↣˖p8۶ '7r +Wx3| o($p+  + BB QDD! +aBFqB@( b@0  ЀDBX&H8$ ᠀ÐQ,HBdAA,X,G3`*. levJV}_"h. I8pf:344567Ga1Ya* H jb%Z-*+(D%jhYn7-؂5X_ptM63X^[/ *P +@ +P +pHɑrppʫ /Nh53I8 B8 +D`aA + 0a!UXEQp_Hzkj I8L&Ip`¡Vw7I8$rM֐@SHp"xƒp!Qt`" 48D8 *a 4( 4ؠ ppa * +x  L T@@ApLP"| +*Pxh|atЁ6 "8 +<@h @ l> &@x8 <DTP:@0x ) "D : B +8`( B + < Z|A  8h PЁ @` `$Bpࠃ`AD@:ؠ&!p8Pt \pH@`@ AA>A/Eh0(8@$!&p SA 0AH, YЕYX+ ,AH, tp B<` 6h :Y 2@xP 86@D* +T:@qD 0m 5,4@ALV$B LЁhA"PP'|A +D4+!Np Ʀ< @ 0(%𠃆hЀA5@ႄ +:`( |A@@qx$Npt> B PA % e 4`@qW PHqt|tx"@B +8u*X>A@Ã, P h` .H8 6 x84\pAZ480p B|N 6h@Q>$ +S +pA,sgT,2ƨ^KylꤦdERadjfuRA#<+few#:"_=FKXe硻awgM4NB^I: +k*ʪrsĻNτu)ݰj.]>'AD$dyD6בclۧ1em1D݊:'#";UfeNv唫LdV#9}U)%T154fTDvҋgɰY%Ļ5"{l<c*:ldȫFA[tdϘPMR'Wlts\ 2c]d̰Tؙfg; +L6v7N>>FXhUS±[IDNW5ɪ0/.;ޭXX,-:q oKH*nN^vN2΢a.6,>ּ`IEnflX:25\q/1[z+2EVEWfywe+eix:[ >UU},g#TEb3UǵC4bq1ŦFUJUq.=gd\u1WG"4^2l6g'q~ 6"?[FnEWR,4ʰ.1z܉hIfllV3Aa^)x֑uhDQgG3X7YJLTWQթIOh8w;OVaKU!dqkPZ23D-^ ZsEJq9*#e!B~aUcr$G:&TvCX4ܪr5T|dBqnX2lήdcfuG3`QJY]ds]VfER)Qpx׻ضq3ő?FQ/jF)QQ0Ys ݉LŤ̽eyBřalS5ElsO*e&Rz4Scjvw⎎cKFn\3#3vfة~Fut5Rg.{pXuE4E1"MQڋ65̨hwfc6Z]7x*N<6+3Nǩ2KѩU2jT3EjJ܎YH*ڏ5UՈ+$fku3!+R\e[|SmVr:Oߨ>\ݺH[$ 1+"#|7:c_sPI&V}lfQ.PHgظ,g6ĪIe=WR\Wj;/[DAUe?~Fb͌VyX:̢ mlQAdÆY1T*l,fy=Sk:6T3"U=[E;!%I[P(6UjQn\ZciQuGcZ4B6 +ͽQN\Rfz=Tfd=4I~$j{F>,}z)2*ȹHv2Ȩx|zi,ƮQqƮ Ed-285͓eak{8*ڴc=TH+N%2QՙNEdr٧DZlg8SAc|j|O2uSQ˜O26Z )i Ol=|-A7'*NJ1df(-%*VA3/J~Ew֓:8vú2_*xQӪF_z܂NTK} in`Kc܂cΐR{x-n;N*hh c_2't-MFG*\|lZL}ܙhNHxwUSI+&F +yDS~qc].Nb/"ӷ'T\QDwI i4~a;ctJÚw[! ;fZش[ס!nuSvvKײ#[jfĊ}R2fCN](6,Zg{Y0_fIqXu8 08R1V_rAS}HjxŋOFT[Nvb)BgMQ1ǂ_sNU˘tѡb|iܱQ]e3TUMc11Sy rnRị7T,tarDdWCȉx쏸#G=2-ޞRZʸ20}<);!QJR:FEkE&cEφXUl81ɍ =#.+A;y$-rXTb'SO ҫ?A|(Pd"h0@!beH BM瑍Ǎ\D310Mv!qDivlhW"bI>S/3"^5<Idm :la&mרb:v_\A;)xRTt(g137}lƓFg.~ U Oˇ(oMGK)E;8捩r% mz*2ma7OІRR?{g8Xȉ&Z)L-fu1;Έ'c+ϸb䬬Hͫd3ŧRV2GE 9UR/W GqV2^2Sь ca~f\|\ 9μnMZT|6h8#b,Nհ v}&+ˍ :v+uLid mhGŝ[%اfv`3=XlkN]6FaV:"Mu:E" 1fwnɑK<+KWr읳aKN̉ƍlT;JUfsIcP_܉Y\y+nږW]3C;<ҴtZAVY +: ?芊8N87G5m łvBTxʩGƕT#c +*hAGkэsRF!1s779?N +QŅ>7GW*9 +1sgqm]E⬌jB8`nG32HEk3q3oԌG]a$_QOjw?X~udGNNeFEɭG6ňuDJ,*H. Za g&ތrSTUףth*?<-|$No:|<Ĥ7߰ DNbtFTYCV}f[gCroxXϕ*Vy#?jx"Nq7hw|jTcvMtK{S?yP$bJq"[TYZG *:9s&[\9a2ן[9uְe1cUOr7浞52s)vu#*$bȸ$vai{]KŎU4}RS7ޑt0s"o:sN{ӊ͈B4]HgWLx5jaFzU _OvF=r8f3j6s%w>9YuqSnӓ*Bi1nO)eraWj*&sKw2O.V4Lx[͌7a5>4+>-ʕ;+=YI[ݜQfFow*e'bS +5\']cÌnwhǯy䉶 T*QS/C\Y`*x,y.}jЃXVa"*z@!7 +:` h(`@` Ƞ0/54^fCVv6Ztg3lTynё3ZF[2%Dǡ"d6yY~3Y &cQka߈XqECCSWdbn!G,:hۑE/ؤzMiCsgl|ȨMimDYs7.cF#sIm09ٜ:vm^&R\Gcsٝ?cÚLg*hsGLyE2u2. KĆo#H2|*٥tߝ~N6=6:ug)чej}rWqF.m|5uUɠC>N5}5d_Vy ::l17r?=1r}MK"*n9ڙX\W'VfO%L;V]#w%B,TȌC8=n#WQ˲Q9 +L>Kvew7Od^FSdoJL:ۦДj^ݳ"oou6+:!SZZMQ2Hdc!*{5lDW)WnʦڙXRr5D&RmEGەht4Տ ^TUw&0bd +LIrvWQedȐoQ.SG=y}ګ̶d2f8N5L?W,s=2ۙYrȦO"w\VBK͍I奢Ǖ\ڢ#GXmFw'!V =wÄ,cjiFdkj+~!kg}ψcEg+i˳lZofe%OaLQ"k{ k=ԢHM֩~y BdF.hƼ]ϏRFӢ.͡iq)CcnWFhH>)RJ|gѐ*Z[OinHB72RР;GcvmWΨTn-OHGe&!]4}fSTT9v\gȴ(_T≑#Ȯ+}HT!ϤTtu'yUXKdnSsgs#'D6E'hcV+G'=Їgvs~sCgGQ Tf)xɠRfjرd\YEs]t;)c +:4ѺZ݌#mLO1.W%ZFXEo[ڍy1+Oe^fbEVӖ>^77}舨>>wUhuL|3#o'VGdWcu*nWfì +ېrV֍G.kMoU)Te5=,GxUqהzuXUtSFӪ|UP]dvHoUrU INLk5luOa6UC9~"EĊHXZ4*trO*G4Ȉ8l5o#<+S& +ʯ|36Dl#*JLc2|UܬUrM޲j͋\5h&c3U8VTeS#_D88gU'2XWw$錋U~jw4Km/MZ:6S~k>3NUkR<)5&y&%Yh&syE9V8z8H2Wx4*Jf-2֨ +VFTu:HU]yjJ M Vnr}jLglS7̭IvRwòb:iIڟutKc3nc:խ6VG>i䜙M3*>i{FJr!.. K #ɱ.Br4?m4FJ!sY5-S,C1ߨ.,%2D0˗qO;޸XlwcRvvLƓ63T掑F=&bƅ\ba%O|24ϘKB1vcuIhc7›!ըө;1ѴrŸ;h3wPِVjMuXFR*ҹ}y>ԛ3G5I]}<$iZlfyZ6U[fNf,wOU%vsDTwK"rC\3MLC<݌!3FĪ#]*: Γ#q۩tX&7lhCT<4gumRb*7\E$[+v٪jzV<m&b"~\H=Oޅ&x$ء+w Y1:vo'28ɝkv)mR$۰^E^FVs""9j2SJe)qbb.Ө<)NFIgʺJٟD+ЯQnJw +*foH6waqYYdFa6=qݙ=~W4CAǻ )>#nRlȳYOqN" b؋ZXts#cÈ+ޘrlcʯe:Զfugx4{ m:J7wO>i6R;:}yz #)$=_/5BVv+6+\RvB +ePkYkDתQCp6Su3_43kM{itl&f)u24ʙHܙqHPzv+xc>3W/ȢbzuhhEj8󙐔˲|.Ԑ*o2)*2grF<὎5d{i|?\%沌m棾H{BXB~\QWcnQV٨ }|"qmgH$!3!) ;q4)V\Wu5ʮ,ΐ2R&?v%e?M1:ש+VV/ǭnn}\ro.'n皕]2ǜF7ZT;ޕnK$3.w1z\]TQYMW2Yz"-ԑ]ﺄ6jWGut% |39;D6bel.{zօf.n~-|t=de?G5k6kѽdw*|lfzM#g7|V3lUpӲOgkXwNrl8d͉ZY̵& M*"RD4brk]2pѫH\7LtcTzM- QՊZ*:U%s\VS 3VR=8:VYmeWF;ۋgIẓ+]U]U=eT7iVhv%dPͥޱ^ś3#_.mũ +|uC٧cVR>E{JMUj>nuk}/nlh)19ͫ?qykrX#"HlIsL=湍YRs8 +<\0(݀ +[2VX#9o_ JsAdϣfy%o*AYu`8!e_CZ);?,ևҞAOŵBR^E%'T,ږZ?|PoPEo0v$C4a{k` S/2B0͢dM~g401>7{7Og+?4eТ8ݦY(eS:SIy λ,) `5tIq1V^/%ho"?8$:&|L1ZP@?}=I _*$;E #[A3ֹ +o&O3I +GBٲ?q;(Pdt_K{E$g?TJs<(0B?*6(zԚC+4|Ñrw$掽,臗]平zqE?vUJqW":ZQm1K9a O~TFfujx쇦[M@Ȫ$PiGI-!B%\/-]J0>QU~)Y(BI/Yʷ6Y.OA{ ˨Mx6𲤩!4@Up;s\4`9=~zph`A3 6 +6P/Ү(+0vB^DuAe8}@[?tB-\ Cuί_], GX4>zzzKGxZtO0~ŭ nnEX[.k4K:tG?A@OW?v1gW?V?0&83:o&݌FWGm0AAo_iݠ)ia.ס%U~î-RvX2jhB4? b\%oúE9Tu7?Y/0LFa `>`ج:a[ 3(@ Z3wpA쑹ZN}l1~C4YV4|,3D'j~Eq/azOVJU'7+9}2;EytJECä5ʟ>Cu>ǾND8@Bt`X9A]f/=iz>{VZ%Bqm>t>tL8(>ͮ=Un[ ߝK[ݢφYvZ|}pv8PX0/N);`~wV6ZXun>W;&&3|H +()N͇[0V̇ f +|> +f>0@~]w Qk>L$|(:_ Ӵh>΋՞IR[9}8Gk?PCzzr$s#c NHZB:طIZP%>p[X\<=v btQ>t?)IWGF_tlT +on=sPeчmU6&=xDuai*>V kxVls tңt1.NM HcHfO)ԨcovpXm}'E 84:.3s:([Z!@ +A~Ǧ^+8 +P2zƫ'TQo'> 0D=|3r}K",bnFT0t1{QUQ=ę~ 7R Jn9% +ۥЃl?4,0uQB!P  =3,Z<,kvy;Ȍ fa}\h2b]ByZpݨIJh 9ocYV)4ڳeoGfYu1:4QT fvD̬խn Z]9ӪNAW|ׁM)^ubSNN&127C'y0tW pFe5tⓗ^u:iI  % +گN@:҆NC::$p؄ jԓx Q6G}wܠ:Y b ٠C'cG (PsVM/~9v™"X ~щ4ã(beǚ*-5L Rd$ haK>eqN|- /㎟*q̯H'<(cO:^QnN@8+oA %4ߜо*li&(S|Fv9`l7'mmNZG9$,*@oNBoTߺQrrF/SvHN:й9 x>炒_͠ڞsMrBC)@C`ރINblI5(c3sǛ/nVs'SNPCO-NrDž$ n+](Bd(uig߼U JԚi̟8ͨ6N4NSn 9qr }8QgcҔ'*cN8x}Ŝ4KMʎ:agI<:@(4ƜIJg Yr.?&뵦acNĘʐٶR&$6?ep "wf1'ST~ӑ`Nm`e-sX>хǃ9qxkcNBNV0' iUcNFC']2B'p<⟋9A H|6 7 ÐPǜ7\PcNh<" 9 ʜ\])|e2rœF4y-* +-*j8=k}Qq-/*- ^T4R@EŏyLꢢ +D,Eʞ?u +gR,d] &R9W4Rp#:2H%:OF!%%!׼!tMQ!Wʲ+GNA*Rb#HEx dIO5Rba T"YPl!ǜC*q+OKV!LJ<*+έ՜]Ge]UXzBjЭ``Ž +2ӎem9\Q]ƁM**_Y XS<6NwV;Q R}*tT9IvSJN/C'qTÊJz8Xᛨ>l:Sq&*yi2n:f*kP)IgpHDLG^;v@$v+@T(PQ_s"|(Ie$P;Ï*/~EpF)Y(LY7<^/)BU@JT-Dڃ;)nvSQ5䐚F84=E$J)PtQht}/nMeSDߝ[yÇ@(AS QdSVlp]lJR vU4e{IWESARiU)P )[6}))w;l +SFS^mIte<3G~$z)!05b+!lPy<㷰,uVj 7!<%C71 ռSD`4,S +o)vO ),{פx ++g+xւ>Ź9sղ)<QD}7Oէ0^`F>Ewl6*%e# aB OIbS(74)C)4kKT" Jkb&G|<"j@u˘ iz}xCW] ȀK0{'6Ì*#KT`cG bW'TRs}@EjM?0 T I>_p0P1#J%́J'擹 +b2A\60PDwČPATPbn2P90B + P-0Tι5Ԯ!RŷS-*x&@ZGO\Iň{H%U))%!YbMQ~fBqKkb +_sFe#Q9bKBCDGT GTd=3j/ JS*"2T]dDVa"`W7ңO&&iQEiwQ KME. +H㮩@R({&eJ2JcTcا#4T'zYVe.%XޠTLvxJri=1qTa4:ZV*?5\UgޥNmw¥Ul?[, *Pdթ2A_Z1zZi(8r' VѭV.AP`a?TUÄ#V%* 8!*x>V.X^0@d7\VZ\T+Niv(dHTrgs]W^!qz@:)+yiD+>V4HEJ@gz>`I"1K^[1cZ|~iGW﯈:D[L_c\$y{S> %mB"q_RV+^͚?WR+Q4\s]҇2_3W[VF˵LeTf?0YUOy8k_aι栊T+|̳+ z~\Y6_Q3[鶈@$Ȍ t=+O2St 3.r:_A_G\jW9F WHܾO+ɰ@_AhĠgwE$4|]|:WT7C翢3Kz =gH*9dDX/7|K5kҁƲ40K+ҺxksܰK_y[ueFL[ v~$|GUD6`` wirUW`R2/C̓ +'Ryh$!QFep-Ġ Im]x 1 )%P.f8!,i.짬,e~' ZdK5;`;@#S O׃!,2( <$|rkX:׬Y  4ai4aQ0c5,Y;S1-8g rFR3ПaQ.j33,׮ain XrtZ +@3<70[(ML~9A^kiJfkϋwWXn^rW&WEJvZP"BW~|6M1 vEH2hw305yѽ|B}Es^-}e%!8Kl XAEXyie,bAb5-c- j"Ye[y8Zٜa @eܾ P>ղ8r(m>ߛK^WѭXbY'fť\)Y_β1p,8AA,Y N%0Xh1nP`Z~hPK&),Zf|ey-'MD%8"+Yi}nY^}Qmo[Ԭv +\ _ĥ4uJT.\_]WkTbc`e!qahVvkvvQޮ]#7$6F! +"Nzz1p̼g^g.4kgo4Go!Z*ɘ"j8^[w5aO%UQ-S&+]".XP 0ⱮA+PDE +I ۶cQlAq*r"xtzy |MPǪ?k';}5~{"#X_I>;H"EyXAWݦ Y~U5SpAM!43F)Rs9J$ܒuaa'F倇T\(Lf8<1&6ڲHf"n>$53]*K9j@hʺ$Z=6;5`h(.mi3ǜT1BBdpjlt/r,S<\đ +?:ݿqɣe&O?ta*2C֭#bg/")Yq-*^[vW\.QbmʤN,e%j/Q<*A0D00/zv 92gA-`LX4y}{ >D S~LYTvA_v7`U=1bs'l`}´SxGYUy(FYliܩm).tvF`8rcG~$fh^"fh(6 .%3] +~u-WIn>ס %9<:A`<ToZ' Hu|;gu8I EZC ~jY RSwOLm\"N +s@qhSo*LNN`7g]27ֹS9c=r 2<[>A[_02T`)cY.r9l)hFZ:dJ@ԤK!B֬_ znymC#iy+Qd)F^.ƉdO7Љ.߲hkR%)hLOj(JM-`d(@[Kh80Ң4m6SwNiCk7Z}L, +7pKM8%>o cPlCtX0Q'/]ielhӒ\|nמXPX)!t~!s~;u%YМ ;ÎP*Mrqd8[I!Re)!nGȊ써Zr\kӮ3I>rV޵pn#PsU$(YdUU+g|Mq>_?tRM%ܞ~ӆnfʚy#4~y:\Jp(;eT\=PQ[~(@eh5W}|̨㝎^ACY w,'^vR,f0ĎHYn2ڭ!.ju`V'̩!&!-漵d/zd]O_-$ +@Rmdb{~ISF-l?k/O| =Uom="H1^Sy*+NH祢X&؍>UB#3lNjJN34s 86h5Ip@e?@# +a[cI̋[LT%$**aX +\"ޜLAI%:0*1#2`r9h>Qt6TqղŶ HXrDg^ P Ys 'ȁo~{l^b@H~ճ3^wrt$f|vCڌi߿*؈"0]e +BJN<T\,G*H9 p* M(>/*od qޯIݫp`(_[9tb}!A!=^/Dm}%byKm)]$앵 !Yl_(#2vdkrr̎W9fCLߝ0@y3Tc R.QqnU6d<eR'5=ںR?zii+y#\sy9ü &Wd9`Jscy^G);Dm >"5wZ"A;-F"$@-^R>"l@-?10N6դ@"åý,W9zZT+= +:CO<\%pI  +,>[C=0qTkBNeSRva_ BE9-7_{ u6x֕B>Ad~la7٤T6c\L ߲NXmQ$Xc>rdU[ ݹdCK9"Fdp[vaMF %'[:r)խ\ <0>ƽMxjhR$*T k]b 렯dlc=B0?()rk,+w^gsgє0 #d-iֳr@u˙|sJDFeYjjόЃoӏ7I|c +!i|@2Qly4{0lXe˛`{K"UEt$ߗV6E'_K>*q+U=-fQة T"We YQU j>!rY+n{ ."-+2 E+4 7(Z3%$yi 93o0<:LRX|ΕQ!sݴl3{G?"JE{Tբ; W m?gwv1 4!۳y0kV݁{#Qّ9mAҰf]"-)l=)c1spv_dq;B/+LKm#zp1_?-Q*bWaR"M^LO>@g?xn)' Fit Ƨ,n\rDxJܺh Q(u땣cXCMHGXUa Jd 5t_kφYs7Tq e2 Kd"f + ! {W_Qj"iy(:I )(R]@Nb؂%R=&5T_ZON> +~7sU ڡ5 b }yj #s։PPJmӉ(fa0hv$0~JsCj~H ChkU e. '~NЊ_#BjKUZFDm$O-3$gy׊3N]X+egǭ6#&g chg8>QvAaQ˞t(f.>Gݕync`Lax{VCC"w!"c&(j0OZLorP>22U&rvmY t=D^.^XU3L\FfZ9k7k{9H:OO~}2kVJz/A'f5Gk.5X&=\}x}gVv*DQnF:q_(uBijuC~k*Nڌ)ꑨN&QrM92 + BЋV&;ݜ:V +3 5pz +tF: M7qBɨWWRW>rY_xEГEu3 C`y‹Jo}o-z,zGEެEؐZ}Z(ӆCy.Xڬ{JMsz^ NE ^IȠ7GI:q X,f({@HwEXKrph62)h0ω"VHI$($މU$mQf"h]8!`?pFM_X0h! pyƣc+LiQ)j@H/XEڥbfiU%Q fATļleeFDgy١l22Fm͔d XbO6[1in 2dR̺k+/P/I,4G'fcoʰu)9Vqf=;M$A7ރH˵"0 BZQ?MMI;q4Du_pbsUM-f8zH +hP6~-6΀ P"im/܈DQ*[RH+) ,}8TCEmy.3@b'&P$έ+H1EIFCHc2N OуG!2 0P׍ݹ2*0#6{uW/:K>&} [QXH7Ԩ?e?ۦGJE :p<:^yaچǟʇ,IY܈`s/cg&]Xڥ6u6̈|H,T*.$0o#Y7" % k.J9[i 28@}S%ճIFݍI@VVvN*fHkgrZRp3^DݛPװ\;JૺR}Q +*[-  D?^%mK+Q lP}6k5k4H|v·/͚ +OU(5"ߑT MU/  zoK\?`u?8laKYUĬ_Ձ={F]4sCPTz(@š)8 ArRpv*'=ݦH`/gnWj V_X^&E OgAy8A{XQK"b^VA)QJw6iؘ.A;kѫgR[T3FZEE|YͯSI.OD{-@4}{EfH=`3322M \0ۘ-pկB6OI V-u"zg1+ zo/(+`XSythKY53_L|P)"X\g~AՄ@,藪$>z7 v8W*7`p)UIP3:6ץ,NHmY"0fb:w`|<`7ܞ[v3 ׊-L>RőT^9gOQw "i@˖9@g7/`&#q!ZoT8'w`/B&&Q_74M,\N$zI5%ldKb91ib{9Tfvowm8|E^}`Qé ]څp[=?^!(CWnQ~q+})Ksn +TQ51d#7Ҽ YO@8F!1) TI:0~R,̐C~?pLD ۸W!uoe9[s AIJnF=!F/q҈a۴*xx&5ulS;m7JeL+1wxyW`}5lv:].Op*ŴYCDN<09!]Y,}fI\^%.#. +, +cF0o1Y!wyIQ6"](Y{Z e])-p`vG$c[]M|ԏN%*/ +\cAŸ44'c (F>aИ"VZwp!6V9J3bik1z0-.q䬳 +tIW{M+[1fیX3F($5[Ԃ r>uv<;r?cv&bȠ zh#&;hMQ!Mя:&xz##ftN*GhNB5hokpk.HI F5 zZ}'7[._}, iRPАֻU0_CA`ИMxS]ש禘bV%WISpOd}xBR;;G AO܁YăY1P~O[Ң}_r_DxHeLz 0B +=)q48#]ŝ+c4r5`(H`m'DմGGqH)U3l:T'¨Cn#gmenHR +[ Wv\ #O `"Lfոe^ k +dK䋹c K;_%]rXI%r5ܳ'ub]Q{;A)l%1 iEyx䤩Hwq3eD+}]|Puy@PRixy<"t7PFBAۊWw22hHx{H>".Hqͣ騆jCHemqLw?Q~w%_d%øt^ai2Õ{}řk њoÀx]Vj$lF!0ImXJ߉^HJƢ{٢c!%K3Sŋg)Di$o H sR D{EyoECzhvs؊ϑ`fϫ9&oR, _-˜Brrs4oKD @P6t]1ڝ#oۛv!.L}h q#ݘaŐ0qdC:h +cQn꿬}qZ7Ε?kJs(%_ȞuoW>eӋփ:J짇]" "^OMlc0x!_U`P.n'O%T&|G|.|, rS6BaAljʐqOcVJ tq|#ĈNN 2DYu^~|}&Q:Go ܗ`l z^ +r-RXw5uTQq΁lO7,ad8_Ӣ""fw,"h) >BZ }[𯇱2[CMdw9SXť)1I͜ F(&]5Quc^-e5FQd ]WnPbO(jZjdn\ vgmQ!R΍V:q +T; -CMr6$@mCCC$6TMkCE%`mYm!6U*ԘɳM&.]LIf[ cqPi,'xG5~8os >9:\I'܊kvG_OɌ?eע ,0kX(s/x>vu!EOzg+ԬFةwl{r@k x- %J2+yΒF?s@>NxVޣkJ1*I(Y3T֙<|e:iep]a&f pr]`x"~42 փZHT? O&]DXEuR 4y|7M\Mg'Mɕ7 !K|ࢥalc+7Mr <25ot"o'7@n/X,M,sk_W2!+-tpX0EZS0JD6Mij +>|ӈB.AzQu +[ G/ MB+mn-@82./ +|]UK%_%d -F&DOk C +8;[X2"i0ʿ7YB)|ϭp8b/ +r><xXyӹf|K;LOtRHN!& B}est/îېR(DJE/8P) {̥ icT4N0M` 槁5C~sT|Kzk +;G)ꁥOC("82B80)'3E쏍0@*lyFE.6Qh{N'i |>d.f`N1= #_E'؂4 tQRK_a2̭vR1mrL=c 'ԲloKH1tT!/o& SZ 0jaP~1@3CGAW/ eǑG7unq3N!ꓒ wTMYz\P*iegS'm0W*gG[O|wюJ@Vt6^-(\-;:leN-$'9ֽvqO@x0"ą])g| m 6q7w(ܾ>Pf^=3sފҷi7>^PR%Hg@d"3hX&yv]Ԙ?m'$[}mZ٘V\w\mQI""7DZw]hK*z;ۈ鵮z0t!6@*O3'?L k!{q|,I]>#n\F`0&Oh\ ܈ +<p8Exl T; gRgǬ;jŃLȟaWg`:4@4G~2åL#˦pt"v}$j…iz7f mZfd9@qӮmgos*nv\&7ox}:>"T:66 +۪)t?};`FOkw#ޓD\L7j>8,Liq@)sɴLh‘?'0g(xTTj}Pזrnw`ly|? :XOdBD/W031_ª$[NO3prH3Hp\]+hr-n]m#Ǥ +{[zY4s(\$WQҵ#ќ +'s!c2EC j'L/9]X +'9'q^Ž.C&R8&"Lr&wCψm$ザ +)0^ڳ^-q6, FA W`px)IXh[f9I>4,潁پ3sEbȠ%L#VQJ/+оL*o.ÎRRzeAn_?-+JMDaZA|qj+5D"H2€G{5cޱJ*Q 1<Ŀav%AV"/ +1AiʼA = :/҆eȒ0EJVi-ˮ/ jWw LYzM=@a.__y9]qX2;o7ܣoS2kvմR x9E6Hc"SaL֜a*4L&ĜU2.fԱnjcF:fs̨d.'Mef̶wJa|̒ըh^BEÈQ +tWx3`[0x_#2"[XBM%bgZYKmo|[y+4+Pv"INv9s7X2x* d$*u[әlJUc +lJO3;+*v őGRJXzs}~ːJ>< IPNP"aPCz,R> +v@"0ʝ\sWO)ZXhNlTVվd~uKUՈ~U?s.i8FpR2q_H61%رW=I+7;[0.nc- @/!i*U# +:@"h/ ~ {sL2 +4}&/ [OSb YX[qHTXQ+&rN*8 o +Rqd0 0v> +` +ԁƉBz0TQD~#=xx'FX/~7 H,_;'Ut6w_u}@S8{XN/!W_]5|±P@ &dn(Hvq&Zѽ>[)`C#[CَGpQR#R:,΅00vADԂĬK)!>Xr'MX)J@"00M|8;F8AɗRS +Kk9 N'=fЉvl_ +Uѕ{) +e[h"un@ʠ'k$MtT'c- oL}UII-x!$Pc@hgpc[wpl٢=EdBM8ZrOaڅ<4-Br u-"6-?+ZWF8@u:/X_m(\wuSdα[r/s _jtU} NImBxa/Jn+ZG;*ةi-r<3cKأ=U?+k6O4l*fc<)h獭;6n x\2m=$7H y';$3&u%'K";Snْ{mg򫭲/Vܲb.C4ؙm_ |ţsFF\czÞHh;\d"3lG:->اf% o9y) Ig^QI<1w׻nvs3%¹-cFL剭7Xx.y;:<1MYnhcYwyvC12o_l -g~W—ۧ=6]=,]ī=:yF!]>>z[&hHѿt4{bO<=X5G|+>Jk2־j~k#q1yE\s&Wa9ѰZ_7Wxw<5dNJSX5+Vה%Zh\=ZgPǪsNSSyd+5nJDSVKRek,%5"/GbIhfdr!6Ş'6AW Z*ZX2u79k+%FxBcq$drɐU8TN#:ZǻkmL: + +S]ҡ~4zZ.v}ert%T&˝L4*v] mewMJ<]tHY%ιd%E㕄e Lf!fRreݽ\CxlF'tRϦ`?v51ϕId&B2i(g8f +777s/J͆N2:͎ufJqfFt³uzRCf^*,2JkFN݇wK*g֥"1GBa|!IuMc92NGLL9L6ge)N"ʑH,TrJ\*֑.ӽ؛_$G#g gX?+/x9Kl/VI˗X">Gc1էGZ꒔^~J]}DHVz#m6bU#5_(}RLtf$͈ΞnqWG)FtI6qYLI<X1d#i+Sv!uHU>ю*)͛h>1^|$'5WLy73ܵ^ސ.cw#J婤}i޶nGD-ϽSn=6ÒU7%UFn~D\^kKS8d\WXf܊1(2vnwX#5B%s?ʏm~VQ7WWIM]cvMY\3)kDâ1kwwӯ^"5һY3mC5#bRd;ZۚERO?R\UکY,Y5Fboz3HZint||3DZlUW5;H첽$T.QQV6EnnhB%t4 woJ:Ṟ#ԙ=?VIC&2U>n3k DW"2ߘ̼ҤyLif*Lf>_u/65uGguLɍ2cY5SG.q71/ӕuN\Glwk#g^gǴXR-2p>u);w髦*D"ԚQy#~vkCSuhH;u"֏jsP>;TrSwtcrjy\!IJCvVZfkNFV+)UG8B2JFnQ>ehxK}UDk#rȈ-G)y Y3+""c+|d̜k/H9;=ojSDkV6UT,+{X|L'i]'Eo`9rMmn#aRD֔&&suX {rF!2_LM9Mrt䔛ؐ9yb5Iih*mb6Ĭߖl!]v$;m[Nފ^sZcG|OffWmrr95ei{ΣǞe'll7ҺrϮGeT1aQG+yBQH4ܹSUA36?4+2Gşш󾺱qx'xXϕ=?WǷ_=ؕ&D9/sS_]koDRۭKgi8kSdCZ+UVK&KsLX8sX%VqH⟬u2?Wΐ唡ɷmzoz##cV9ח4uv2#o^&ҾJuHFUZSHGY]cu>s\.RB|dW6id)#>iU,wrjJ4 &4  PQ!aѸBD    + d UH!B$x`dА.Lјph< "*0$TH"0TH!P"$ + 0Q bbaba70 ,   0, +f +024` 0q!qA*`HhD +QHDC @ <0 (8},4D0X4CH0"`T0ѐhvAƃ ȀA0 $0L@4L\p  xa,.Tѐhx`Xd* ( ƅ &.H@ƅ _hD,ab 1b"&ph0B  DH@ 2 !FXH`1Ѹ@a@$􂆉*`  +ą  ( `xh`b 2Ph@<E  ! CC P@ !!bD 8$ @5ՕTlI+]wc3 4"6*DI1C1(N 0+&$P8I聢xw܎CGэvLPM=ZqYbcjA?jsr QC*sHWS (,! +C! +gY}6p`*訩4p07p`8PaX +` h(\xp 0,dp@Dca1P@@'  *40@F (L84"p@c4Ď Dx C0$4L4 +0_XqGy7+ +WxKJ웣PI4XeXdI36[DD7i=tK6YD#R2OML%DOF\Uݑ)ҟ%D.%R<+Qmb-g6MRꐲf*/:L.>;JN!Tg^& 3VS6!O/n_kwT^T*|9 +I&$(u5C;E^응l!DrTK,_ʚ+[{BH4r/' Xh%璫hTad[!GVWQ5u&c$Y&$OLL3ܐeJFIyu}uIw}G}=l g4R4t6Շy$C25.aծ|"+Gy}S۸oīygbd%rIױhk hK %:D#S0(bd^&H!1XLl !!DR2UKW*Li}kz0$Uknh8qU#]bfp+YM=,#:h_\ $|cB8q;|,cS`tSMCb).5XpMP/FuqoѮ[:^NQB-&?:31f4 !䰮:ca/_ ;P~K=[5 XcRs}ռ"zx '/"M/ zbIfd,p:Iء`_P$3*m)RnO2A} D =O*Lؓ>@"'g[sI˟܄[(fOxg=~w܈Yc2L>]DVAupXZLt@sxc`BrXo Fr|bg݀MCl 8$/s=sجX@+(ጁSrw^>KEw"?Q:WiY@ݡ+L{{3'S2,*^UEt4 o_/ / lkD4kILh nh4%4qӹ  PF!]q 8o]of'j;Ji i-;5a-F~[('/Lz aSlEFxD#R{r3r-'f=w=F{}?}ci@/TtK{+SpIb?d&vONW){K9 +b@p2}CskUmH:ŰL, SlwE.S":6JvՀYW>xGŗ,%-B%ST/JeKԙC k9e0ү0WvLA+Burň}C@Pއ]9R.lNdBJYl+(9Ķ[kBO2^=#Q{+㽣[/Ap8YX_zL,8!N1}Ϫ `uA79x^2YQa YQ,Ksݮizca28l ԽXb 2 py*cP~Rr89ĊD3 K"c/K@ |=!L\LCzy6_Xzg'`5El!³g]Fg2KtcV03ŊYk>t\ǢI*oZ+.mU0^b_1Y܈L4:afK4b Bn]h>n*l+rzĴUt +8qpgk:^nOʥJ| 4Rz1{ +ǍAk{_M"\Զt҄#kdt12=8t=!3hy5#sQk7X6&[G_KQ5tNJ #D) uu>ϝPPC)ϫ'Hёv;o^ +Mz_1M-JP 9Drklqv]sV|\m#%ȭ G ۺB\[$rCmG7YCtk1腛AW&d(]H[6fs3H$t#PjqG"Ӎ`01)xm |Px~g^76ߕ0ҋ]RVzѺ7L ]v;؇:̅WbP*|GOQ-^_9B̵9"∊^]Sa.á$& SWY+d4J@ElMMES?vP~kj7TxγaӐ 񁐾Q7.šVPu0Z|(}䯷-L)\ kur1iڢ"\J< +OmS2MӐegiL*sK~ +!dUJ1V2lAnO_$Zl*OSސv7oC7>ネz0lЅӓ)/!rߨiKmK(_i-exI›. e$̭!Κ1aIt)nHtC2tLqF ;5Qf碛69F.EgiC&kV`{Y#Q lb]}ߡ~̯%|(UiO P Qr8կEF[a_^)_җ#H2a \LdA4`,I&i.޳4T}BeE +~ wz+ol*1ýzi{ݭ/$G?]%\-VYԿ<~sV)D1`?)zd0 ,W@Gٟ%ɒ|?Cl0>/0@ܛgҩeKDT8G꺺gfg1|Cz:9īt8E@ӻCDQz+\ݺhGv#>,])/2/ &o3XQ +5"7>RJ)eeyYñixت20&=Je-e翿;t( >-N` $u Hc`P?*̱G&a +!D*tWT( PIE7@t)q5#UNm GBgcPtK]ח+6(bn90:s XɉY~B+ )sMģ3:?5D{WEQTnvXf"R'*])ͪI b5\VUΦjt +DE PXMT)n55Y օvɑIڨYj|%ǀ%9-IM`y + j0Whp5 <45>&KmG&9OA1ڑ+@sUy'r$"ǬB +ȉ&r6 ș(ܲ̀ 5]=I]4=HSIՒD4MD.'5xyk r,|Rr Dn,#^j>ȑ5A "g +w7eҭ"䌉aRs#Cȕ:A+9!71r^K5d?$Hz lRrHT:@eQS-S0BPUSve:9PCt臚LLNJPCaMS5f5 +aRqr9SN΄vNΔNNdE؈D9 F#: d4SnFhӼO"4V>49n +ZN8m 4¬ySNP^Y-Tne%=æ4$+)@8ir'Iw,'\rf4l @rAnC+:Y(}y|kIi6P`NI1Yl)~9RF؅ܢ0sω&IdG,O@K戦?.I?ḻi3hcF}Id64Q ՜5#i x4?9 w1TSxnmh9@K,I@4DžNyCÂ>lhԨB.]b/WtlMXPAd331r'93 +M1;S4}3k6̄9fLS31$.&ڛ4S@htƸݼTfhWC|"MBCh8G(k h1S6ȕCYE$-4?M|эsӟ gkhbL$]HhWFǶHwvtk)tQFIiұh$ 43),084VW@I8t?IyȢt%AtIu|ИҭNZ4Bq4F,Bѕ*45oCRۢ[hTm +0h@7ùmR hg0NNݠ!ZN ҠY$BrZt4o~t,t> "Jh<4L6TeK0OWu s"u1?uԩ!yTT@h1i:!r߽ uc}}ihfu5fhHu]yZ~_ [h+4f3;8]AADh(BP>B7l7pwtʟN̕K[wv7Zww% Bhma-+BaIkSLh40m{J4joPpsh"'RE\ˡ;Fߗ+%f`!94oF7@m#۾;C`.u"r n14;vF2 ۟DzOQK1K94)(jQq:X/4`^{^hw MX884:1 %^9y*vu(Х>"$} YUA49\8EC_MC@R4UͳB {#84 春ǩnЕu| QȮe|G"fhohZfpnΏ4岡H&Y.h %y#+tJI +( X"p(aͶӋDs^Օh$KYJ#J1F$AC'EGUܢN2@heZN'hA;Bkbxˢinc\@g8 9FDZ jW䀼m;<L\`vmOsC:_%0`Hy0վ\]@as֨zEc 'Ij+l-p:ّĀߪ`Kȝ^6smax$6ܐZ9kg,pxf6Г1xEkL! *#q3W[+8LE%ygEq!2;&k|`j+ș +U%yъj-cTdW( +N7Ro(V=*3by^,ȏQg4_}KD+} +wlS 3f2WTB,3b)Mg> 4%E\n@ZH4DeFڗňE}DA5H)]2-U{9}vI ¤Δ;OÌ3 +RE*PHsʫ2d.&+]Ȏ"ol+:^͙{,S 8#O c&M{xD0V\ +}2Fu#&j(Mp8|A~@?/4B:#3NJ,Xؖێh P0PoY<ŹIJ}jc1/ۅ裞NE:&l/{Si6B7.>*4Az Ra25+\ q.tŏ Fn xmC:vGעT=kOn<e{,Y@c[N&s= + +wƵ".zj 0׆AN Gx_wRDXr:"[˸@Ű΄>r:Z> ^TC2$K|qC=8m%蓻H!G"`AP@ Ye@җV y A:S Xrˁ&APx{fw)Q:77l8Eykk[P^@X4&E@kr2ѡy6Fe&%ꨆbP^E)4֞7~Ta.YA*^Gr,́~5Z/ +c ?MMO(i^*5 ?&0`G dOЫPK&e8DRFk 5<.H E<\ +K6"$ /SEz28`9GN'=/Ӵ^X{Q,8,Y)C"A߾_'\cs:W) Wzo6/#jbXLRzb 8Ųߥ2:ϢUI0N`G]'8 Q=)WVͦ7&:CLW+$ MeI5_K,1aw+drRQPd8*߽^eRb8 XlQ!d뚠imgVG\nLo~ܚizT$wGѕwF dT<>x[@*ek{::9FAgπz٦]zneg-2K:BqM.qn<9j5==?DWMd0o5L8kp?&Gbx슒@͑ӼgB?1hkSO}q3ವwTE1ЙȰS`Gm-)# eK,8gz{]CuH<_2KW>hh>uP83k(#b8oe +7qkԩ*LR?1K17AO8:4 t91Sȉr{ NcM3aAbs LfvDZ,6ā8 6dM -x%SS K&n^lă9&~S8&pM'~;1TvM#&&LuV3VeJ + +]ǹHaY ؄܆ j%aDrgr#*FNF\oڰB7E1/vr<Pu(_m"F8s4?E"DګR /QwZp_vN`ĭ)9e~k?0u +R(a +L1C!lɕu/_-ƈ\t!18ٽ2F8IuP1Ō#ВAE]F X@FP-OF\s=$A#<5ş\iXF`hlGibDj,HJ߷/4;*N#?5D`8F5?O,iACfTg4"១"٦[VF 5}ƥTȎM-4F]1jlF\`a׈iX4(ѵ׈З?ŞF 4Pk5bvomFsGK^0z&ƻiGΒ\=%#W_'g L M@BnY5]֤2-Z M`, T!gE\)Wn }dEGۉ@oɥAS>LƅE4 + vbc^"".yWhc#DP>C(QW `*))%]ǦJwƈ›%u'ř~~)>(٠M5=)M$T"B2B(G5~ȇ!@,0LmiӥCA=uPhI1,wFH e?Dڲέ&{Bpw6 22|sph/6](4Lfoդc7q''5(B%U"gxskGq%wt8ϵ(w +Ws^gK?P]KV, uF5&$uZJ5h-X瘤)ѣHy8c]8m$DIJ膮ߗ$s/o\3ɘ$;?HuXh'%#3ٱ +\ؕnmEW|zIȓ&|@C=%g#%" .ZGX4 +ro2X'O&V)TVci%x$U#Cn oC! + X(oUI6FXh ?cW +y!E +Ez0z3p:Q\M!nkCtwt)!% stDA톍D L3N"3)oSb˙) ] j^*,< @M-F`Biv1wҿMR:qh>,D`m H{XO aonrPb&n{FZj_ +%M#.In+ +L J"Ƭ\qP_"y2{Ř䷻6 ,ݑ!Q7nmOmZzӣ[%PvՀ31W /b4z]Đ9qIVgQ~䜕Rxb f/Gx" ֽveO)[99 k,_o5>!A#DH F6WH.E䓃JrJBka[\ +SQC\P[QMoqVQ!Ռ6aCxkpw5W۹O勳Z 2q>=JS笵o{M !=ؑ@Yԟt7t_n{4mt4499JH; < p-O[AoB97=wFQPco1s̟&;U c]&;ۓ~^@mAKHi;^R[i~ v; +{ +=丑f:Y|1jaD)SM:OHRq5L E,\Յ2y"KT,Z ُpހt +l@-1rb D- +o/ =юHvYY-iR*Xi\8ըv͐6FO2q?kp6@%t~'8H) Ҵbj6l0֌lLU/<4b m#rჁ 4`/V},BO$Y@2SUZ 95B}AN+Z9v=/TP kzگZZ"3F06P6'eR?= EáOD4J%lH>y]2d^V 3 +L#UnS@ K6 0^׋鞀OOuX *ACBŗ0/Ѐn{2"/sPi g#t=kȫN'eEbϧ?9{%ܞ.$8O!a0?/aOO-siOo3Rah 7]A#uk*V $ +$3Sp>[gC#T!b+9ZagM$H/)b)^VJB<d8oO"F|9+e@_Aem΁޿kNIEЮ=A#7Ja+Bn0 \kj(6L< :L%ByձY!I<>g5VX_WKI``\T زHd3q}߶i`^c=O)"|W<;093i tڪB*kV8؊J߀T1"U͕=e ϴ5_8Dz?VVSo |r396z+p]_NKT#oDט*ވ.,QlۀȦ7S@^sH; zXx9'N|hɑq/w*@9l4؛:Z9ũ[7/ă떋tIR$# cJypVˤ i݅;.]| z|<F'7"+tNf(Nr"I1hJN`QĭW T4g(J!-Jx]$1]SarrD$KQRE"ҫ mQ?C$ȵ%(5zPD"ܖ+<&10T"(e6̄G,^g|k-5hDL] zル$Ρ!8#퐇m=p D8TB>]* {=@:yB /"u!!B)FP=,mB*4UW"t&7)-U\u݈bD,1z6"`¿l>eX..D߻@104@kǶ/.ʇ^ CYNԢeJT4W!rX|C3uCxQpM>DۮhY/C8>c*2whJdunjnATP9Dw" v+CT'"}!@8' -/D"9]"1!!`rǝy#͆+%Ne*Ѭ F@R w8P Å)CHdl@  F"!,"Dg4F%9Da |r\&CPq4!ApqC@:ewXռaO @Jg{Ck@m@n.6DA +,HGɆ(*[!`q2BhDhY !z<~|lg6Ă~{ qd-!4;K_NhLchwhF$ qFrJL\P +&% 1 mi1XݢZ2 a0nW+ jR!qtr sÇ,vC8:QpH"cFsoOBW)e6W^>72N)4}!fW`J(8,;;,';\e%(b ,;z/몔 &6 CX<U_@{س|pD,+~H"w!vu!a0ST-Kkbm&\GӞB"B48J!X`C<`LuaCzhݥ_Y Š9g(f;;l"MxS6O• 1NKҩDlôtZq!:V24`Coy$6̆ly+ÆWK!J:li6F79!nl5]\^>7yKolJFc RH)Y@j>_ 9F҆5NyiCȵ=`;+:Gh *0N1}BYi +1gpt.nj!L_04> .w+B4*r&8ˆexD{9v5`y* g[,Az`OUxxC1J ڐo.>oaK%XABQ$clA|ᘐ <#0$x'Aa7[Gϥ8I"SrrpϾ"+td%IAע.[)lAN,_-| i܂D[+cB_2J":;.xjLA2E\E)Q* Y``.L'"<ႀ+!D}27v@DEMDe' "N$* ($A/p  +2YE\ .Rr\ а]Zi8$W!tB|6~ F&`9<7բ*R@ sTrrRe >a8-IHRV Ȝ%ðQ&VװVr% E`;#е:L ^_3m24h ] ~FyBsv +"AtI~V*-;QHY+D{3W4B>fOC}ũԆ!*=TMHu m|\|[z`xclfZ:? }Iy}h%mZ !lfmҏ`!s43 >\W)0[J0G`{ tZ)?ȑ7FU6~~|KƜ(Z)G44SY"ba4SH !S̓CTQj8h?P`ud<:]JFNݢޣt/:~ @Ά7ESl1r.C#[%/Ic(?"?#c?Dh} p;(C8s-卖-nH$?x7u5E.~ rAxQ~Z/s^+y\bE4PK)w ? lEVܛ'8h> 7q0xD !6Lyg@߹]5H~` ﳞELG{d/X <(􇘙T̵e蕒,-M_ |,4ez_?\Qe !8"(6C]`fu&* O k[- S->V>v"nJvC0ĩU>UEl3$0$0$0$0sj")*E3M;Hudf XUfRK|HT\]j6=ޥ%]b麑H.+GdFi_=]jXdF12a5B(XH.eh]iĨ8e_L$q@,7nf6:hɿS+3d=]6$r–-GꮋmӤ50;GR1ADÐ S ߪH-03c +Ro 8SE%J0j!B8BthloG\ +Ry53q0@q(Ǣ8RcG{I5Â8@bbBAHhh !Ix`` D4L$BC%K ,"",`""OD $ @=@ph@BЀEÃ8HH<` !` D4n@b!V;&UO4< uB3>Ie˔Z/ݒ"`]\F +Ʈ"=OLiT#iF)X +b8•IͶu+67 rn +oƱ L*we;]i mwe6cB;UzɐpݪluYfպlTIӽ&5/umKA1mbUϓJ3Iѐ\C kO܊nH|>=_$uIr*u%Rh%McM\IJ'xF)ڒT7JRˍ^d)1\zCS;DZVf增ޝX*vK/ڷޘ-U_wR4Uo{moٕwxVָݽp@A4 "8x@3$0 +!LHL{pp h 8D4C~ gH`H`H`H՗~A?+ gH]3$~\B*.,#7tl XL4<>stream +>|eŊŒX˄GY$d#.q,u5M +-TW:nE_gS6#8:"K6^ +֕#'yvdE4*FmDT&”iNjYe:]e{ZVR|bFOJhP\ԥ }P[0I1˞e?'$ۣVICAZ4C H@`EZ8 G/\,j^Q[m]ﬢ/ُ7[gv\{}iy[ ϬjhUҴߍZںYus׮9؞ȖõkT*QOJSYX[3ջ/,ik3u=u ^vTNmK%8Yք?/UE?Պ<  +r8|bǜgZI<?(p7Z`=;592R1&^IVT!λVX`̢R[IܔJf%N4A,*%c;M~ҡfw6E2)iʭq!Dd2;bC˥akNtqC;"SR!H[ldy{~'*~ۖk?o7V +=/~J?d^R8I$RkQN:F3@` だ\21?z8R4Hq80 00 !BR e-lyQwŠ٥7\'"wu =ofkAq$D>xsWrhg!@\z[|.5Re>}G4 4vm>G71| 2Fe8^ArGqO(Ek oȀ%R0'ŊDE(Ci]=OŖ6rj0V;(b0`2_-lyd/>lCyQzބ +.wʒ~DW8~49NSqP>v!NBI ``7w*xcg;T8Lla +Sx- :VC]s|llK@2dռrF tRGϚdcy99Y\h0JRdE_Us j E| +U +w|y"wB.8[E"p.!,`Pսyf:z"DdKi !ZډhvzV!?^"zuЂ{_ +b ZP%C'U30*]\#{K# +˚ +̰J.c5dQK_0<}2e1L #E1^+2"(sXmrY3I(YdQc}V +} 8Sl4 -eAG«ҝh?WQmҰj9sn!i ~]c{Jyd][r(T2 3Bt][-c|-m^ ?fGxr&LڸAaY3e5 qq%+Y-](~ڸe5C+1Ts/>;FC7gN74h}uwI EZ +Tgc'HpwU+ͿI @`Tq"Q5I;[N}y!biuT[kOy)PU*Hml 3t~Lש ѓ56(@cD7K%\… Q&A*XHM3~F5 :D'\h +9yTw%Vw;g/#v NFJCWՏ^^,ȡQgZ~Gd#*]28 Xa(8Je0C!xyWs@vfsA%pPϘ} kW[T +N1.PiFb3F&Pyl7̉KjoLjmTv$ˇF!2f½{>oHMvd#r4zey`3o% BHƨkN3 +ڭd0z"*Bb;wx%'I'F%44[ˏA:9/*7@2gzbTV%ݦzd뫚}o.7%unP1g |n"^z0\^T5g,WKC6,zJlN,%sҥ ,w|gɡL"Ts$~bЅ)mP k!0:M G/3{f6&.%sÂ~dzZ(2M +@ujWTւ\N+L_k÷cg] \J֔WiHC@6`tHYѭ0(4IPϞqn +dR_Spj,n׳A^p!08aH32jSv2b<&֙y$VǶyMg>J)m5EGq" <82Ԯ K¥K $0ʖO#ܮhC_P@<Mk^&cd =o[vvPr +_ĄpN>~_}j#Z +<"f_Cc;7(M$W?Z"[XnZ* +.88=TM>:ZJ]xd^6sƐRePƈdhF2W̉>4m&ih9˄s{B$,-S%_ vˊNzzޅ #%Ir~;9&"u/Oɩ| < u#4T h{fش Qp Rp-j")! V: ~v7|<-+PjU\LG,Py J9<+a_;z`5xs9ԩ jq.I7N|_Zu 5sMWCغd\xϵ0gW|\42b ,ԨZECpόx.sgfeF!Ͳksd5S*UOQ?GZog:" d[J5P,!l>}Qeꦶq$%P`](FG+ܩCVF4&߾#N\i퓶˘[r:O +_^{zOvEBʿڎylNI5؁I"F[ )]{Pjx0VMIJ$p@a+ &_*0ηaDz~]pٲChSoSs2C<DRTYPzR+0&0l52iT"0Nz)T_,GY"kVIqOE&1w <]rŹU[es'sO׬"R)$0PL-4! w~c׼/KYQ=Xv8 ?<q7㵞r =e]qpzo(H72D.ݿ:|/ W[鬃J;v\aַ\ ~~30th- N0<9Sb$fO[{HJ QaEkAչdV?ؓ N-h݂٘!+aTR̂ĕfJ{(YPu iXۂ"Y]h,!p;*hT )7{+Ԩ:zWPpg|M@rdHP21"5&Ԇ0Ů$xyo0  o PA <]~Bha-VI =$W^uJܨE :Jf6:$EFi+!=&v\\J9_E"WXn†P[]qJ*{HF i۾z59S@c+h%I)6T +1):Ւ`s0<_JM۬ӮqhDʃ`ʬA.?u;(w^(4UO$ +MGKs|V?ha, gVaԏ^!f[qF7&!bSUS7W"Wm4e2܎3/xNlS$j g6=蛓aC (Ԉ"˅mS@5]ÅouxjJ0pDž +/?HgzƭNu|Χ]N mC +vM 1/T$6)Ɛ5ovݎJ>͕1C=KVg/1.[,z1_*Ҝ?ibC.u[@iPxTVubN\+afWP q50e)gV4|CVoP;,T^ ͝P5(5!U՟&p;>8TV>F5魘<3fy!Ab?*D姎1F_?[ў ASM}mI91krŢnUdIТڮqXVJ"m'u:^ĩ\jbcndF!q~x"$7X +t Q~^5`+kqCc+u]GW]A<}fa+u5o;>-t ta])n3CM{3 v2PƏ f`Nj;j*ٽ E 2m<]AA}"1a1f:Zr$_& gQ44W1퉀YeNH 76# *d. ED^G`E`op#1 ! METBӠU^K(X} +AK@(x˚kpy*H%T&ܬt{mwraYr|Xel>Te37dzRPYyCI5Ao^/R@Mr. i810_B\gzJqÚ\`P:mYtd|P^X(nz +/cJz l /ǚv@61^/S,O0ho`ӃA-[>|6yZ8yaܹ60 +O*-MHlŭf J &Z }$9 Z x3c%T"v=BZPq]?NwVdDG E?H pEkZ[2ZnΩx@y!{Y'3LM&51RqN)qX.QP%om-_O%Q^6Ήey[nCƯ i +=h,w$)pH[ s4vR\$tmsF#^=йg;y:{o@~ ^Z hO׼h2c/j*t¢3ж槾bwH*IXRN(ߓJ:-M={ɫ ;BHbWPpᥪpTuwX>3  JR<flP)u4ܼ ism2 E'BNt_wc$p}ŃOxߚgAWNyPN_ (du4i +a8HA 3HTl%k&ͦv͇2]czBy^12&'=AkػBm˟KĶ }5>Co0-e!}NZ]Ge|{ǔÖ%P2Ͽзԙ+V?gI7u&n5GM:Rw[B=XlZK"':uc`4~(>(v8.B,a\5MdAVJK*Ԃb`/wZcFv.m>.HV̹zÌ)]bB̔n1zU5wx\AIk‹p;y2|v2uQgwNۇw#Ѿ:aɞT8oh=5x(0T&W`,O ։o3i>P9W/g)KoZ-VfZrrgrBKlsn$W;ܛ˄ e?. !JZPR=&Êѽяqm7!$ŸB?F#иU ]]J!&3r 1^hT-FBzրEA PwqpE\P!q&I^?khvꆎamRFCi,(ӏhOީU"g:O!Q28E a9#/Z˳~g @q(3rP\h:34[x0Bc-+؊OyC ?RNoUF̰g "eNx_nG֊t KxӇvct!f|D(z9EM *#\7X{lhJuy +S'[Ygl(+@K,Kz +O J: 'XSk:fq31D'T KԒh !sdE:3 e/!;}dBV3)bxM뼛TM`O?ʊW8 +#V %EĂX89֛&?C1є-@gn/"jr``K+!3Ue v{/P9&O0Ye{7"*QXhUTEx2b5 +l +3QAɁVw( ^2v,``vpX 񋩔 FZs֩B׉/4 l1Q\qW=9I]=t+;.|(' I!V1^=jz{쒰 wg'Õ&_34qFVKQ{[?bT1t0GO5iX 6LcB!j qcN j4`4=~!L ?G5 \H!6WuN0fsmbAq/v@ de4C7iui7O=*^^8]R9[IКr/6T=Jѫc'fCU-RB FPL͚GMWl8j6[PMa1&Hhغ5Tйݤ0x`䀥CnQتlCy27;b7BB^]ˀQ}/ XF{"8^49J .(\Q|g9 8,XT`#'ӝQJ6|FteXIbvinneVNLf6v3˝4nX:ؙ6]]J35yWyŔ(=8^ڍG$#X۾ 6a9yb~8Y9nڙоՊ躥}]X\ +=- Nrn 䛈+9pm'~g/ugynrlR&O޷k,D 5YKd 洯;3,,z v_d{,X1Os\ o}9R}Y151nsLZ6Ǒ|v,ݚkBZWk7t;˵xҾXo_7ϟ˝`R +'7=/uKṮP-jaԱv#li‚kuZ@;Vd0n7|[9&]q k9p5 c Xk7ceo@uխhfWjqM^Al 45Ki_Ҏc}.֝ ؚPVVMKIҷ9 g9ӟNCTno34M>},y~s!+/=9=ON/ Y9s#/Mﷹyy79Ko~Hn=&~I{?~QCVӇr4]r< Q7>77'~ʑ49wO49Dm]/։I?!5Wk)npw./M^h4psdyߣ{w,p49%/;9?I~r{},Mzo#K<=w\}'ýK,Mno^h{93~ w>7{Y}7m͖r2حYحnDXy^3z_G,~]ܖvc`VPHL)I>4x&K$8ߓI 9=of9~,=9&7^r|ܤ??/+д2hp \PUC|bӁݔr +wJ53{ ?b&cDw/-v;`n#`M%IU0UZ4OZ7W*yêH!(]@!('(Qڈ@y*TDB]kR :SE6;fl7` +"(Q/Mvz$@ϩ`u!O460b _*LCAg%c4@ɡIB blI8>#* +WЊ S ED* |$?b" ybTԬM| +øy)O =@)NB;C-t4LT:J"E6߀vdĦv 3ɶ!M`SmKj6eBf:YR$LBbP͈N\&s_fh[9o~Q`V1[㲐hmrć.0!,­TQ,L"dJ:3(aP莧n'bL]&覧脓o@_V@Qk7ЩE1̔PJXV]*j0I%D/SzDs°FDVs&F 2 %gbrQ Y32Dc( ہ(` 2AE@&P5XHlf"c=6+A#(eGj o@@FC(2γ8 p0L\)0NtPϏgx +gD㚝:`~u1E4y8 3urϰRy7 X`$jQP|dB#R$&Q \0l5^#o2YG4c#m ׆{ *䃓Z߀=&MΕϨǓZķKQ k8uDJv=um_/}.0'{8g>S \Tt8MqZу\DvSg#V -&E# 2HpMı3EW(|_8 RhJ47 LrR*٭1 Ǥj5j,5ثil6eDF61*b +(/õF*n0:KgV eȫ|耱ݢ.YLS^V.mb_ׂQ 6>_,l(Qh67 DFtqKadN +-(hmU4ADn6NDūU +O rob#g`R%"x@) 'qzX*,$I&Ng,LtV#XM 3 kBDPy0kTz U#Nb@4LaR4nDكk  NDo@H !u+&#gߩP$,E|3lD~#iM~0 +qMtr{t_ֲFh3 +)*MF14R1 8ebd jTFFC 4& !xYz I.|Bj"Y[[sU@( +BR0*|SLe.OC) $أ%PٚYa,( R(\0C0IB7 `̏BͳH1w5D,{S`7jސ90繨6ދYa!xlP`/^Q0z߀Pp=XZvdz, +rλtFL/[fYAa `(TruqT/+I'U 3k0SJI`D@<|:tt魡 +өIlKgUHM:¡v % Rt v~AGrto9;ϑeGP`]F0v$;C]][Ҽ}j2S4\aWPQ5Aѩ‡fŢKDn9tݬ: ](t]u_LAKa"l3NX +]uD7XtSGy뺮3̕&J\`^W{nQ)^ՈQa.D6f^QYw!J3  X4c^scXo|UJ^ ,XIox.4!T`L# +:j TRBTU+ ,aRG.;M&<ٱl*`$|é ĝ^`,,6 .&a4Euq 6Y~A,PÎʛH-CDױ<]=K뺎@9c߀xa]7`Dk3Qmt>jH!Ilx)sLoGL&UlbH2k캎MVAR፮F(8I뺮wh򺮓]1~.V1S:/uAfrFOazZ:mU]uJ}]׽UfCu%no@2]7`y,cPq,YK<I6]yMc-)EQK|[Zhb0Q*r.]U_/V\s.g =:e4sg ngVavd\}ikkior}p'&-;1)'{0׫ecv,Ƌ~{7`wvo63 +nClؖQk*Z2ĥ}m& Ҋh-hw[D[#V*:ՐeO>G[=Ylk5$}VTC~5#ER䈀"kD[GjEo3w7 djCaM> _S qiEǍ^uΨC^%"^u5b +jgJ"b|еuR7 X'Xl>H}uA7IwKgcUⵋk@6i@]Bsƺкi=nEA +1]pdј0* q#>9'ԱԱh=Y,n n!U*%M >q=(MVjQ:+e⿩HB1|(C?ABQ Iu-BP 9mѺO?ϷD䈨ā~Ť U*Ѝѽ9tK\ႸbQh]6CfQ,D-J>+]Ei SFT'FA.L-faIR7ӨxaۜRL)&>.p9QY1FXutγXSA!&  t dycO5ҩfCxç.crDf9vaS68u IqԫXҮ@2*虖`BT]xBFF%":ga6.N!h͐lj7ţ12H'Sw1FAeBY!@ @aZPN2OǣA1WZ"e"(}""rikU\I1ߔ7 0Lt+l'j|#ՌQ54tmE*h  *JvXImd[h +ױDۈhPjrymN')# ȤLb'ZDXE%=hGB貲1DVNf&BiI˺o@RŢs#mEs&*peW\0rwp8X gY45BZ~XX{5솵 ɓ +Sp谆cK 6q_k l_}_,r6 !=W~!l~!l~WH^XXñPސ]CZp\ߐ萖a0 f7#&aRӬ[)9pO<W8dF n Ρ+.]qA+c_o +]qP> j7~qeG=v^®m6jkvn[J iX̵{YtK1r׍^+,Wؗc9j⚃-c첲vVcf_4MB r8d99jY(AY, fkmf96D]mf95wuG-M[rf-1fuGcn)U_1:yӺ1[W1#Z<[`5VØ]i+ckr_`Lךu+SXʇsLմh0K]Qdh䘔`}3)\*mc1&ۈL7 d:0[,G.xvP,6K&ÒCUQgg,V HjhZ~C&"  dK4+!: *(( +ET6IfngfWK{R-,BP Źʛ?(c$bX|Ba}PYfJZtHѿnP\t owXZd][ wZk=΍mtb:.D`tI1t +v%~G@jS1IO& >?$e|I}d ψְȒv0gDVˌ[IM(l6bhUOf7}P,DrD!s?If+x2IxɠWz67"t36=.}"P!E+17C+wG[@UWT1V">xeF߀a~3l@G§E*!H݌ Ab\S6c1=<+Z -Zn`X֫yV⊘7 2ri`Di\FX=" ~ 6;1}Y,[R9-˝F4$P2CPPu#r$e;/jEBL >> 튧Y7kL 38?YKBOȲ{76 +O3D$X +yN#mr<㱣c6jSN @(%dRUdbt=,Za,ZE7I'lvضYkE#T +)5㤈9"74[Gl2<2!y蕟!*cBh}t# +^d"z.[.$u LJ@)SR Ƈ 7 !?Q`8 + ԿkS9:f h,BY{!oN|6֖:lvBŝy=.fdшFT'WN2#Je PPeddGymRcmsb:c-:x-*p҂c곧Q{`Ng^.ڵzN& Lccx̦Z'o@!b "&JDKiƨ*|"VvsFD:B>'"vN*ZGhdHЀ)#E|qD2426w<[x c,Uiߖ0ICzdo nn%@6S)>-dp%f[$:CPpdi>0#ѢXNDPbtfѱfP]i%hm8L0So@x?*LPHkGWt\DT`YMVR{8$IPb85Il Gp̬Zw ŧ;*UO-dj-1l†b..Fƥ5.ӂF[A]O& P +z`}VT$:Na'`AszZN$4 +LvLbe@:1@+ + Y8`87>4(n0: o@ fnT0ct3&4*gUJ*SrHnmᕀZ^9#Zj.ngY&a: NjkuBN Pxif&ʦl*S|"zr7Si6U' +HbIۑ=GQ< ߁ bW16Co8hgA4]x7E!-C4#(m޿ 艑^21 (|#^NHXȈB+1umҲ }gHtw1Mq`ׅ},0f;}MESY bVUzWGSDsa!P?+e4j3L-"J@QYX x1[2<^@$ZZ4~N6|X;e+?"Z"Z&9!^g^M>:ԎX<53inhn:\ &HӋQ6E흚q{u1 ;s1X qL\$9#nB;|ԨMLM4Cv!RȣH7pboTuJȤbai +!y?=!ȵ-iI)Fj'cNcTy&3H—C?dy#OC6b3~Omϸ-7\qɁ& JOѕ +t(A}E柙9^Wy=ֶݬeAVFCƋ(ZRLЫs3ﳤzfw,){Y +dn 8"K!dtnsE^TV` s잉G^Ram93w73kzV Ǒx٢9No`!5iIty: e$*LTq+^۪1)9?6mɫu_&Ў,@lZQ?;AS[$o)}dF^zvYg8 rG\ڒΥT5COg_RUX +*P-n?@i>S}AA }3@YȁRmNM1bx0Iҙ59{Z_d ?{J'{CRGIY1E>/ݲp`"9ŹA3-$BmtqKVb{%KwҤ;|1)Ct4z2 6]y؞(r IIik +Iq̊D M* CD*XjZ-Y & Ps?A?X1IQ* Zwoz=VgAGN7*njp=dǛy4W%4#}S!T$r=a<^PG Cyqh [3VFmpdc/)k~tW iLͩy nQƓ=}P7B\b#EX`~3rm=5ϣ[I8hXsR.ÎjH]cApۼ{.]I1P>1&ERi;+3fՋKrBm'pYxl4GxѦ( &#Gآct͠xʵXd PgFY>R (Oo.Z݋焮6WIEyUDp_V4< .ũ|_}\^G=9f xxLZXD9҈R{)uʨ&[`4Iq%ZƯ^eYF~~8_,S9Im+Tz4YQ1OM +tLDeS/%N36+HN*" =OCsX9x}g Q^034!ނC<& Ѻ!E50Z >b62;"‰EL@ 2(PŒ00 +e%//,ZqڢUnqJ"#4gmyA-C@:dvQfvp,fd4ƤCGYJjea^D CO,yWLl^dXl?s#{VӄqY]Dd70+Vp}%IʕA)'5Gyѻ@ '@;:Ū?Y:(_UC(RXvz""\/[bg;lRNI?#W&Z#K#LT`\m8qy Yugx /x'>!ׄl3R9}Ӈ&_Xk`s pD& `jpB9Ff)Pw:-8!-"OՓ7XdhrP-wXۡDiPvAxSk2JCfgW3+aTHXb_NLVwM'Ĺ\Ӏ4L)ҜMU:Xxִd|['c2RcBN$<9;rR3D`0} wtJYً7rbJvKF$$67b0WHDBSǽYpL;g# Jӡp>QWDNm4!WY7xDHQAũ8 =ߟB +,y4]opK:;GY7X_1Vk^].60t}ZI1AGo*b9Pk +}T7c{ ~; H~0ηL%![~EQsQ `ـ@I,Rz"inZЋjKbiaʺ-DTOwH(Cp  ?G~P2uI6B'lX9g9V(Ĵ/^QDf.s%Q?2&m Q=M\3_*Ө}=?҂U%)){,Iڅ/1U(g{\z;7L#D5%RӴP-;3\V-gx[rYp5I ͦDgKd)bG۶mM=HL1o%upMxnU툧7or![ػ&gP ruȃ(AhɾHGF< +6]'D);<,>]G2gc͘c)u.>E祕Vb.ha4ܙ#lGZ1EP!M8`W3#=X|;[AFZUxfݴFuNOBzk(H;g^~?@a:r&TT"CpoW3_W׸mMX +cn,LzDÕC1=Z`R9KlϤO Tɓ^اw>n:Fk{#xSd|iOy{ln k%O!g  +.%?G0;B%Am'&eH2y{LSbR}tOd+hڬ&W6wK٢s!37 1gHcq *O!A6Y/\-Wrm(VdkLX⾨Hi ^J3 r!n[A̒[BI""xBP"#&%sg1Guefm.tȘ U\_ψW7VNԋk4 47FIo<8>† ֣Lc{S-O}/eRA:pfĿÂ`^X GPNQ<:QuPv*&Y:q{8Ŀ!558Mx+T;/ys>pY.m7eܳotf1(Q  r%idD*D $Q@j"6m(=VԞk~O;`V5/&-_Sy>sd`w@ITe5Tyx bosi. +cȻҤBwmn-LTN!0}୬J=ǧ=oM}?oqF( OKm آ+uHQڤҜ$.uPBV4bROaO(p\0֊o3E#y0 +MfռSkE#pBof8':)meۖyfi>[ݗ C(|V~$#g0B0DcC?֙}5zzDugɊePP`уx4 \s6<}к!oiU.ٓʲ iD4zvnW$+ +&0P!a?E"L f!x:*R"db{mPY-i/4 4V kvaJѸWh,BeԸ*tQ7XuZ%<뚀t2ihҡ JSF&VE𑽪oa .K u{{+mmHے.?''&1JB>HdV ̅:5od6PD]t7P6|R23dpG{u dK<2 Pd#x^f}mYjwD.E2g6gHPfuxgt#ø؀y3PFK~KNd:Wc5 b+w|\pyKd%1$sLpht~ø,Cx9WueU'L_e#LZ + )XY)/ {5M"fni`P;Ω>&:@DuP}egr$!ra(e8a K9ўwDpԋM xqE|bξۃb񩓪ɾ1&  k:l"4SE+ Ѵr8%Qq#g* RxӼez!&Q;#)OR^b=8UϽ!<5/qG%7EӦcXiag GDcalvd<3G0 6L]s%=_O9 +\qe!}0^vLyZ+z>}Б& 1vmM8DY#GB~:d? NwN'ARYm(enYm7,S>j6I6፶|&/(ܯIϙ圧[]RoB]K7}Mc6_7q瀵̸1Q6'v-1ï1ɂ´@ݕ1sl+IbKcܡ'r׸rPyx1{` 5.n4HۖYH94̃pTЋ @IH'4тO:av + +^D2זVV$ ~usY(péeg4׸Y+3f!E]&3;+-[g6pU{=~%@O2㳧$p=]Q#V'jԔP7^{+|pÊq8 +Xf gA$1:4X*.pZ/p#Og6+gص0z6K$z>N+l +!R wV9veilw(neskJV**dp4,i7@K9)F#POt\KN"4(MFʊ#V<:,oJ M` %#M"chO<^sP=l.sm 2%E_l(\jUǍk[f - +r~z8V?ɹ#dgX +| ju锉ԩczj~fTp2y-/pQ@/e ,NF$DDQM*0f<2s#`@SsUrS|lr9 6D̸ h+rDpsDFA#?(ja4qDz> j0u8j p}?"]H2gpʧo%*O)02[i8XWa]LN+eB,ѪVsJehd^4@;#ClOۃ8 !\{G΂S=l,5@m D?Hr ouonhtjcN3צM٣ښkYPLsPj\g`yhXPVd]M7<9Xl3w?4ڌ*)EtT28 aC[7ͿEy!<MOU٣μK`!9b;;}-"PfA}S-6QPd [*Awg +y f0,~fL)o6~h_j9)1BU!VZZ}yGp6( Ip+NnX7XX4c'BӲRONq:Q*oie›8 3Lv'-!PZHdj۵! IQlٔF1W뢟lM^nGFqy"2j_n  9GXM0x였 o$Elo(1|*+Q"FG>jALRH Uo:cY`~ vƗj@ac^k=(g +iAp@`D{;R"̸Rŗg?F +$`MUL8𹫰8 +%%53Ur|:WkP0&RX 6Ha6LAKUvJm=gtDHGɲja5 k|7՗)sJd[!MxI_.ϟKXfFΌBE W 1*9٣ITG c B$⮘pѧ^p5Pg@NGL[!6*gHU +ȀCF0MI;F 'DzJ'aDKCQHwsv \T_R5EO{+DOIc XRsF} \q4z!Mtmͧ$|%[O%#e#qMV-:KV<2xEaUfpB σGiel>m GM5O, 7IM + [wZDh2We6@V,%Hhh61N B3[%NnYQ}WLCv4Yng~7w|NjP: +'z8K8_8fzބgo)b8d +w?؅X/;ОU{(*A>=\+a_d\_}{7Pi~?\\R>d[ ­_5J%2W8͈;?VϵZK9pA&]߉!yy +a!fgU*!WLo$@["{vF[=٢m1YW7#@?yl&9̧#lo<"+_Sn9p.^T̯\._YI (ZVJTnuA|^TEn*.ŠiWh ܦ*RK!T`3)iq PPSC<(ː^Ro(ػB9O}cvsCȱʂwԑS|;\J>#AyQw:D6DH +c_l `0 R+GA鐈FM[!- 抰XOZQu%4oG-FԙpH:F& GHˎP2>y)N#F$+|7sT(kh"w =&\@ \ 6~djt3Q!-ayP<@꾇jP_A[icu`a*P;Lɷj!8g2yLUi1]F<=~>IQWxFudLΩc=KPpd6ό@f.8s%ʟNNnxšưM@KA cq na $5J 65-D/(eRA5е`pDr#fNϛќ)*ll'0u 54$;$aO@D24طc8t]:db\SF, Z:0?e $`F}cðKQ~fX})FcXORaJFt&LBYy P5?-U&YXnO+r\A yW̨/y"櫨rle=|Uq&dg&;RxJfýfPc %7:0C]`eȣWK< ;:RFZ>QbJ lXԶ@m+n@!^3Ǚ`o{8:KZQH0lؐ +Wѭ@OЀe0Ǝ|K-l,nTvxu *7 ԬiL=:^hlIYpoǖA[l +ljި YQ,8  ݋jVv+(_,#ÙmyswK=)UR*`/8cWh ,8{=t(}[PTIz2lDPg,;70Ԉhr<3CVZLTșZ:4`F88F͝#[մ!Ͻ<{C~59㖛=lvr"1(uCz.iI )k~As+d W"-$ŭdv8Cᔁ皺Қ j"TFǍWsc#,Qqa:h׸WfhfIb] TP_ùtI8xbC*.{&):1CSdBв*.$ɮi޾!SZ1L3 ʕldVrp 0N |bmp)UѼ S H&W,AFt&s 5ΒNew硬- +w1N+my.Xr4I),ҿ{u"./-1>Aew; CIJf%}2A, 2BLG \FDO/ L#H\,@Ǖ2Ka6X6d4DoL~,pD~ú&F.-AKƒȡmF%:K=jVR +πQ(9DE!MVd ͖u }[WQ& +g>JuŽ6D -|G08PKi}$Tv=0PB +U i.Q~*F Csf +/z[d!=C"^@&f?:SL*c{:KNUܨ*GuWYoL_+Ϝ"Vʮ)A[.vsp-N|R;fE!hԌ9]6 \ +R5뼼ZnbW 瞌[*E[kfVA~5W:rKBoSHx2>fKo: p]Ӆ 4䜉AH T4YHy>;Ky@JԭޑjBd5Z]RyK)omˀ& +w!D."MsqUO7:¶+mi:=t K"nK&l;Mu#ޮkp9b++Za&=="+`_p! +0]Ax]'LTUt3r:|!p )-4R|8@665-wmp%D!-Md:fWJU|c)XiFNW;DZ&hbEx@>Dsr,J0H)|+Ռ=p f4N:oxk̭HgsRs 1Ƚ3Zݭ+W +"Wvh:lLX'f2pՕL6 5Hg"+ ʝxcS/xK5q^ߺU^b'o݈Hro4Nxy]J ki9ч.|MӃcgz{ 3h%`L^ZEg2+8cSqR~ujߵ2Ϸ1?^CfZUYQ.Ya5c]́eBLD}QzSH{i"#{-BaP!4 6hC1΅uSsgbX;ΞvhuƓfl}PTB^ɜ3`P,+BG< Fj-~=Db0l-Z<T"9cVm ؘ' Dq>6Gp(6oo8&5ۼ nȖRf-j,w]-^̯ Ї5\8nظe*bk2s$RJĈ=haȉcuEJ R,j SRTNH謯Y &seg:+ +,ԑF:@)9iۣN8D8Oу;F<ޠbk/ }#LKJjeB֨g'wB)DR^!j|ݿ6ۜ}U}RG,0@Eco0A]Ѥb+e+ +BR.~mid-6]#-A  8US!&_YMJ"#o7'q J@!&wjÜeͅmoEm*fI0 oSԇza!Rg"66ֿ֡n`c@`phRDAmU~7 |ĶatN0T;X2\5mgė*^vV6 +f6+P\'aMg[X(P(i"S#ngrso |ZH>uUܷzl1Y +t5AWt(;>o@XݾC! քmj]hĢ#,߀җvosm}7 Hnn"bHaN /ht\F:0 UƇg;nt*:> +v걍Dw2qN7jE:^XMȝn`p\a}h}(r(-KɪQ((葑6u\7֚\J% +CN&B%tI*eDApcT҇'NG9jD\3Y~5JU,,z Z _&_DAwyM.N@)ډ&Fau;.r[ٸ>H.,J,^E#~BZIJ'SZ+ \;52r[\eܾ.;Z6BdInr8 I~Wv?Y} MiwMCIs_r?8 ㄚn' +~o3^֭]{;!-<e}a0{q8pY֯6pl 1q4IMra=)8sp i9 \b@pm\bT*5 @B 8 +Hr -3`p$K 1`0`FQXkOvXo5@+| _/O+H亗lDX# ds(p0qU'p:c +c#: +.RGd&ܘ(}}IՈ13i7xNA#ە6 Q8*G[C<-`ZC1r`ONm˗eX"*%[ۛ#>Ĉ8V䬨 cClSmi0cDPo:ko!c +Hn(3rmfIz{5`ma$8!H=x@9/|>d5*݀_$$DEDN |oYv+ C/'?=Z)Ym: Wº^>pjn^z<޺_a&QxXE+)̿>2zt+~P0m4 㛻7σ-p‰y&|i,s[ڷNį%FI$kV5Yz]TDgX\lkN|CNJnz %,rUP6Ng`hiߎf3V"3qKLɆx)zvZ}_lqx'-쫬-A'RLLNiצfk`#[)WW=oR]LӷeD>J--_6Cy},瀒3;Tٹ#?܏>©p4Z2i>d砞4;ugӀΞ2Dܹܼg=Sh:6>U=/ :+Bd)}:`J%69~G_xEՃu=zVU!$Oo8vf ;sD45?l L;չs[鋫r[u&M|8hxF}Y;E_tȹ}37w=i3$WEО{)ih2ܵRݍOI߸nIE卌&UwX.ʛK$h<-lwUD[a?(E(aΞZH{kڒ  +pwq!a=jD$fLPsCF_e*A-~dtk^]qJ-M +KJU`T*$sjꅳVU6f@+EzR^zB>z)9 핢4hJ^|Qk/ўJ*}S]P/燹_Xt#1ں + TG"aVoH b;aX$g-CAV5D.Ħjl 4 4]w1?a +endstream endobj 32 0 obj <>stream +HwVu6PprqVw6PH.V0C<.}wPz1W!P L,s@\.]# J +Dbh] X1\/ +endstream endobj 31 0 obj <>stream +HwVu6PprqVw6PH.V0C<.}wPz1W!P L,s@\.]# J +Dbh]X1\/ +endstream endobj 30 0 obj <>stream +HwVu6PprqVw6PH.V0C<.}wPz1BQ*W +  +endstream endobj 27 0 obj <>stream +H$10 IJJALO_"'9 98R}1īMW%@sI~O{7[:UZ  +endstream endobj 25 0 obj [24 0 R] endobj 37 0 obj <> endobj xref +0 38 +0000000004 65535 f +0000000016 00000 n +0000000147 00000 n +0000033710 00000 n +0000000000 00000 f +0000033789 00000 n +0000000000 00000 f +0000038702 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000038775 00000 n +0000038949 00000 n +0000040071 00000 n +0000105660 00000 n +0000000000 00000 f +0000034145 00000 n +0000034502 00000 n +0000034859 00000 n +0000035718 00000 n +0000145999 00000 n +0000035216 00000 n +0000145834 00000 n +0000036018 00000 n +0000035905 00000 n +0000145695 00000 n +0000145523 00000 n +0000145351 00000 n +0000035575 00000 n +0000035789 00000 n +0000035820 00000 n +0000036053 00000 n +0000146024 00000 n +trailer +<<8F0B241B188B2B418DA36739CEB6CA9C>]>> +startxref +146208 +%%EOF diff --git a/res/img/player/body.png b/res/img/player/body.png new file mode 100644 index 0000000..acf749c Binary files /dev/null and b/res/img/player/body.png differ diff --git a/res/img/player/confused_eyes.png b/res/img/player/confused_eyes.png new file mode 100644 index 0000000..56e40e8 Binary files /dev/null and b/res/img/player/confused_eyes.png differ diff --git a/res/img/player/normal_eyes.png b/res/img/player/normal_eyes.png new file mode 100644 index 0000000..a7f2ceb Binary files /dev/null and b/res/img/player/normal_eyes.png differ diff --git a/res/img/player/particle.png b/res/img/player/particle.png new file mode 100644 index 0000000..e5d067a Binary files /dev/null and b/res/img/player/particle.png differ diff --git a/res/img/player/squint_eyes.png b/res/img/player/squint_eyes.png new file mode 100644 index 0000000..c42ba12 Binary files /dev/null and b/res/img/player/squint_eyes.png differ diff --git a/res/img/player/three_smoaks_eyes.png b/res/img/player/three_smoaks_eyes.png new file mode 100644 index 0000000..5461257 Binary files /dev/null and b/res/img/player/three_smoaks_eyes.png differ diff --git a/res/map/layout.png b/res/map/layout.png new file mode 100644 index 0000000..72f156a Binary files /dev/null and b/res/map/layout.png differ diff --git a/res/shaders/gradient/frag.gls b/res/shaders/gradient/frag.gls new file mode 100644 index 0000000..870c235 --- /dev/null +++ b/res/shaders/gradient/frag.gls @@ -0,0 +1,11 @@ +#version 330 core + +uniform vec4 input_color; + +in float value; + +out vec4 color; + +void main() { + color = input_color * vec4(value, value, value, 1); +} \ No newline at end of file diff --git a/res/shaders/gradient/vert.gls b/res/shaders/gradient/vert.gls new file mode 100644 index 0000000..8fa4cb9 --- /dev/null +++ b/res/shaders/gradient/vert.gls @@ -0,0 +1,13 @@ +#version 330 core + +uniform mat4 mvp; + +layout (location = 0) in vec3 vertices; +layout (location = 1) in float in_value; + +out float value; + +void main() { + value = in_value; + gl_Position = mvp * vec4(vertices, 1); +} \ No newline at end of file diff --git a/res/shaders/player/frag.gls b/res/shaders/player/frag.gls new file mode 100644 index 0000000..f6eb165 --- /dev/null +++ b/res/shaders/player/frag.gls @@ -0,0 +1,15 @@ +#version 330 core + +uniform float rotation; +uniform sampler2D sampler; + +in vec2 texCoords; + +out vec4 color; + +void main() { + color = texture(sampler, texCoords + vec2(0, rotation)); + if (color.a == 0) { + discard; + } +} \ No newline at end of file diff --git a/res/shaders/player/vert.gls b/res/shaders/player/vert.gls new file mode 100644 index 0000000..b2d6ad9 --- /dev/null +++ b/res/shaders/player/vert.gls @@ -0,0 +1,13 @@ +#version 330 core + +uniform mat4 mvp; + +layout (location = 0) in vec3 vertices; +layout (location = 1) in vec2 itexCoords; + +out vec2 texCoords; + +void main() { + texCoords = itexCoords; + gl_Position = mvp * vec4(vertices, 1); +} \ No newline at end of file diff --git a/res/thumbnail.png b/res/thumbnail.png new file mode 100644 index 0000000..62b548a Binary files /dev/null and b/res/thumbnail.png differ diff --git a/src/META-INF/MANIFEST.MF b/src/META-INF/MANIFEST.MF new file mode 100644 index 0000000..8cfd9f9 --- /dev/null +++ b/src/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: com.gnarwhal.ld48.game.Main + diff --git a/src/com/gnarwhal/ld48/engine/audio/ALManagement.java b/src/com/gnarwhal/ld48/engine/audio/ALManagement.java new file mode 100644 index 0000000..0cd0936 --- /dev/null +++ b/src/com/gnarwhal/ld48/engine/audio/ALManagement.java @@ -0,0 +1,37 @@ +package com.gnarwhal.ld48.engine.audio; + +import org.lwjgl.openal.AL; +import org.lwjgl.openal.ALC; +import org.lwjgl.openal.ALC10; +import org.lwjgl.openal.ALCCapabilities; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +import static org.lwjgl.openal.ALC10.*; + +public class ALManagement { + + private long device, context; + private ALCCapabilities deviceCaps; + + public ALManagement() { + device = alcOpenDevice((ByteBuffer) null); + if (device == 0) + throw new IllegalStateException("Failed to open the default device."); + + deviceCaps = ALC.createCapabilities(device); + + context = alcCreateContext(device, (IntBuffer) null); + if (context == 0) + throw new IllegalStateException("Failed to create an OpenAL context."); + + alcMakeContextCurrent(context); + AL.createCapabilities(deviceCaps); + } + + public void destroy() { + ALC10.alcDestroyContext(context); + ALC10.alcCloseDevice(device); + } +} diff --git a/src/com/gnarwhal/ld48/engine/audio/Sound.java b/src/com/gnarwhal/ld48/engine/audio/Sound.java new file mode 100644 index 0000000..d16bdcd --- /dev/null +++ b/src/com/gnarwhal/ld48/engine/audio/Sound.java @@ -0,0 +1,38 @@ +package com.gnarwhal.ld48.engine.audio; + +import org.lwjgl.openal.AL10; + +public class Sound { + + private int buffer; + private int sourceId; + + public Sound(String path) { + sourceId = AL10.alGenSources(); + buffer = AL10.alGenBuffers(); + WaveData waveData = WaveData.create(path); + AL10.alBufferData(buffer, waveData.format, waveData.data, waveData.samplerate); + AL10.alSourcei(sourceId, AL10.AL_BUFFER, buffer); + AL10.alSourcef(sourceId, AL10.AL_GAIN, 1); + AL10.alSourcef(sourceId, AL10.AL_PITCH, 1); + } + + public void play(boolean loop) { + AL10.alSourcei(sourceId, AL10.AL_LOOPING, loop ? 1 : 0); + AL10.alSource3f(sourceId, AL10.AL_POSITION, 0, 0, 0); + AL10.alSourcePlay(sourceId); + } + + public void stop() { + AL10.alSourceStop(sourceId); + } + + public void setVolume(float volume) { + AL10.alSourcef(sourceId, AL10.AL_GAIN, volume); + } + + public void destroy() { + AL10.alDeleteBuffers(buffer); + AL10.alDeleteSources(sourceId); + } +} diff --git a/src/com/gnarwhal/ld48/engine/audio/WaveData.java b/src/com/gnarwhal/ld48/engine/audio/WaveData.java new file mode 100644 index 0000000..f1be759 --- /dev/null +++ b/src/com/gnarwhal/ld48/engine/audio/WaveData.java @@ -0,0 +1,83 @@ +package com.gnarwhal.ld48.engine.audio; + +import org.lwjgl.BufferUtils; +import org.lwjgl.openal.AL10; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.UnsupportedAudioFileException; +import java.io.*; +import java.nio.ByteBuffer; + +public class WaveData { + + final int format; + final int samplerate; + final int totalBytes; + final int bytesPerFrame; + final ByteBuffer data; + + private final AudioInputStream audioStream; + private final byte[] dataArray; + + private WaveData(AudioInputStream stream) { + this.audioStream = stream; + AudioFormat audioFormat = stream.getFormat(); + format = getOpenAlFormat(audioFormat.getChannels(), audioFormat.getSampleSizeInBits()); + this.samplerate = (int) audioFormat.getSampleRate(); + this.bytesPerFrame = audioFormat.getFrameSize(); + this.totalBytes = (int) (stream.getFrameLength() * bytesPerFrame); + this.data = BufferUtils.createByteBuffer(totalBytes); + this.dataArray = new byte[totalBytes]; + loadData(); + } + + protected void dispose() { + try { + audioStream.close(); + data.clear(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private ByteBuffer loadData() { + try { + int bytesRead = audioStream.read(dataArray, 0, totalBytes); + data.clear(); + data.put(dataArray, 0, bytesRead); + data.flip(); + } catch (IOException e) { + e.printStackTrace(); + System.err.println("Couldn't read bytes from audio stream!"); + } + return data; + } + + public static WaveData create(String file) { + WaveData wavStream = null; + try { + InputStream stream = new FileInputStream(new File(file)); + InputStream bufferedInput = new BufferedInputStream(stream); + AudioInputStream audioStream = null; + audioStream = AudioSystem.getAudioInputStream(bufferedInput); + wavStream = new WaveData(audioStream); + } catch (UnsupportedAudioFileException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return wavStream; + } + + + private static int getOpenAlFormat(int channels, int bitsPerSample) { + if (channels == 1) { + return bitsPerSample == 8 ? AL10.AL_FORMAT_MONO8 : AL10.AL_FORMAT_MONO16; + } else { + return bitsPerSample == 8 ? AL10.AL_FORMAT_STEREO8 : AL10.AL_FORMAT_STEREO16; + } + } + +} diff --git a/src/com/gnarwhal/ld48/engine/display/Camera.java b/src/com/gnarwhal/ld48/engine/display/Camera.java new file mode 100644 index 0000000..315efba --- /dev/null +++ b/src/com/gnarwhal/ld48/engine/display/Camera.java @@ -0,0 +1,100 @@ +package com.gnarwhal.ld48.engine.display; + +import org.joml.Matrix4f; +import org.joml.Vector3f; + +public class Camera { + + private Matrix4f projection, projView; + + private float width, height; + private Vector3f position; + private float rotation; + + public Camera(float width, float height) { + setDims(width, height); + position = new Vector3f(); + rotation = 0; + projView = new Matrix4f(); + } + + public void setDims(float width, float height) { + this.width = width; + this.height = height; + projection = new Matrix4f().setOrtho(0, width, height, 0, 0, 1); + } + + public void update() { + projection.translate(position.negate(new Vector3f()), projView).rotateZ(-rotation); + } + + public Matrix4f getProjection() { + return new Matrix4f(projection); + } + + public Matrix4f getMatrix() { + return new Matrix4f(projView); + } + + public float getX() { + return position.x; + } + + public float getY() { + return position.y; + } + + public Vector3f getPosition() { + return new Vector3f(position); + } + + public float getWidth() { + return width; + } + + public float getHeight() { + return height; + } + + public void setX(float x) { + position.x = x; + } + + public void setY(float y) { + position.y = y; + } + + public void setPosition(float x, float y) { + position.set(x, y, position.z); + } + + public void setPosition(Vector3f position) { + this.position.x = position.x; + this.position.y = position.y; + } + + public void setCenter(float x, float y) { + position.set(x - width / 2, y - height / 2, position.z); + } + + public void setCenter(Vector3f position) { + this.position.x = position.x - width / 2; + this.position.y = position.y - height / 2; + } + + public void translate(float x, float y, float z) { + position.add(x, y, z); + } + + public void translate(Vector3f transform) { + position.add(transform); + } + + public void setRotation(float angle) { + rotation = angle; + } + + public void rotate(float angle) { + rotation += angle; + } +} diff --git a/src/com/gnarwhal/ld48/engine/display/Framebuffer.java b/src/com/gnarwhal/ld48/engine/display/Framebuffer.java new file mode 100644 index 0000000..a4ea1a8 --- /dev/null +++ b/src/com/gnarwhal/ld48/engine/display/Framebuffer.java @@ -0,0 +1,93 @@ +package com.gnarwhal.ld48.engine.display; + +import com.gnarwhal.ld48.engine.texture.Texture; + +import static org.lwjgl.opengl.GL11.*; +import static org.lwjgl.opengl.GL30.*; +import static org.lwjgl.opengl.GL32.glFramebufferTexture; + +public class Framebuffer { + + int fbo, rbo, width, height; + int colorBuf, depthTex; + float r, g, b, a; + + Framebuffer(int width, int height, float r, float g, float b, float a) { + this.width = width; + this.height = height; + + this.r = r; + this.g = g; + this.b = b; + this.a = a; + + fbo = glGenFramebuffers(); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + + rbo = 0; + colorBuf = 0; + depthTex = 0; + } + + Framebuffer addColorAttachment(Texture texture) { + if (colorBuf == 0) { + int id = glGenTextures(); + glBindTexture(GL_TEXTURE_2D, id); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); + texture = new Texture(id, width, height); + colorBuf = 1; + } + return this; + } + + Framebuffer addDepthTextureAttachment(Texture texture) { + if (depthTex == 0) { + int id = glGenTextures(); + glBindTexture(GL_TEXTURE_2D, id); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); + glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, id, 0); + texture = new Texture(id, width, height); + depthTex = 1; + } + return this; + } + + Framebuffer addDepthBufferAttachment() { + if (rbo == 0) { + rbo = glGenRenderbuffers(); + glBindRenderbuffer(GL_RENDERBUFFER, rbo); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, width, height); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rbo); + } + return this; + } + + void bind() { + glBindTexture(GL_TEXTURE_2D, 0); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glViewport(0, 0, width, height); + + glClearColor(r, g, b, a); + } + + void unbind(Window window) { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glViewport(0, 0, window.getWidth(), window.getHeight()); + window.activateClearColor(); + } + + void clear() { + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + } + + public void cleanup() { + if (rbo != 0) + glDeleteRenderbuffers(rbo); + glDeleteFramebuffers(fbo); + } +} diff --git a/src/com/gnarwhal/ld48/engine/display/Window.java b/src/com/gnarwhal/ld48/engine/display/Window.java new file mode 100644 index 0000000..c7ac8e9 --- /dev/null +++ b/src/com/gnarwhal/ld48/engine/display/Window.java @@ -0,0 +1,227 @@ +package com.gnarwhal.ld48.engine.display; + +import org.joml.Vector2f; +import org.joml.Vector3f; +import org.lwjgl.glfw.GLFWErrorCallback; +import org.lwjgl.glfw.GLFWGamepadState; +import org.lwjgl.glfw.GLFWVidMode; + +import static org.lwjgl.glfw.GLFW.*; +import static org.lwjgl.opengl.GL.createCapabilities; +import static org.lwjgl.opengl.GL11.*; +import static org.lwjgl.opengl.GL13.GL_MULTISAMPLE; + +public class Window { + + public static int + SCREEN_WIDTH, + SCREEN_HEIGHT, + REFRESH_RATE; + + public static final int + BUTTON_RELEASED = 0, + BUTTON_UNPRESSED = 1, + BUTTON_PRESSED = 2, + BUTTON_HELD = 3, + BUTTON_REPEAT = 4; + + public static float SCALE; + + private long window; + private int width, height; + private boolean resized; + + private int[] mouseButtons = new int[GLFW_MOUSE_BUTTON_LAST + 1]; + private int[] keys = new int[GLFW_KEY_LAST + 1]; + private int[] gamepadButtons = new int[GLFW_GAMEPAD_BUTTON_LAST]; + private GLFWGamepadState gamepadState; + + public Window(String title, boolean vSync) { + init(0, 0, title, vSync, false, false, false); + } + + public Window(String title, boolean vSync, boolean resizable, boolean decorated) { + init(800, 500, title, vSync, resizable, decorated, true); + } + + public Window(int width, int height, String title, boolean vSync, boolean resizable, boolean decorated) { + init(width, height, title, vSync, resizable, decorated, false); + } + + public void init(int lwidth, int lheight, String title, boolean vSync, boolean resizable, boolean decorated, boolean maximized) { + glfwSetErrorCallback(GLFWErrorCallback.createPrint(System.err)); + + for (int i = 0; i < mouseButtons.length; i++) + mouseButtons[i] = 0; + + if(!glfwInit()) { + System.err.println("GLFW failed to initialize!"); + System.exit(-1); + } + + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); + + glfwWindowHint(GLFW_SAMPLES, 8); + glfwWindowHint(GLFW_RESIZABLE, resizable ? GLFW_TRUE : GLFW_FALSE); + glfwWindowHint(GLFW_DECORATED, decorated ? GLFW_TRUE : GLFW_FALSE); + glfwWindowHint(GLFW_MAXIMIZED, maximized ? GLFW_TRUE : GLFW_FALSE); + + GLFWVidMode vidMode = glfwGetVideoMode(glfwGetPrimaryMonitor()); + SCREEN_WIDTH = vidMode.width(); + SCREEN_HEIGHT = vidMode.height(); + SCALE = SCREEN_HEIGHT / 1080f; + REFRESH_RATE = vidMode.refreshRate(); + if(lwidth == 0 || lheight == 0) { + width = vidMode.width(); + height = vidMode.height(); + window = glfwCreateWindow(width, height, title, glfwGetPrimaryMonitor(), 0); + } + else { + this.width = lwidth; + this.height = lheight; + window = glfwCreateWindow(width, height, title, 0, 0); + } + + glfwMakeContextCurrent(window); + createCapabilities(); + + glfwSwapInterval(vSync ? 1 : 0); + + glfwSetWindowSizeCallback(window, (long window, int w, int h) -> { + width = w; + height = h; + resized = true; + glViewport(0, 0, width, height); + }); + + glfwSetMouseButtonCallback(window, (long window, int button, int action, int mods) -> { + if (action == GLFW_RELEASE) + mouseButtons[button] = BUTTON_RELEASED; + if (action == GLFW_PRESS) + mouseButtons[button] = BUTTON_PRESSED; + if (action == GLFW_REPEAT) + mouseButtons[button] = BUTTON_REPEAT; + }); + + glfwSetKeyCallback(window, (long window, int key, int scancode, int action, int mods) -> { + if (key != -1) { + if (action == GLFW_RELEASE) + keys[key] = BUTTON_RELEASED; + if (action == GLFW_PRESS) + keys[key] = BUTTON_PRESSED; + if (action == GLFW_REPEAT) + keys[key] = BUTTON_REPEAT; + } + }); + + activateClearColor(); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glEnable(GL_MULTISAMPLE); + + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_HIDDEN); + + int[] awidth = new int[1], aheight = new int[1]; + glfwGetWindowSize(window, awidth, aheight); + width = awidth[0]; + height = aheight[0]; + + gamepadState = GLFWGamepadState.create(); + } + + public void update() { + for (int i = 0; i < mouseButtons.length; i++) + if (mouseButtons[i] == BUTTON_RELEASED || mouseButtons[i] == BUTTON_PRESSED) + ++mouseButtons[i]; + for (int i = 0; i < keys.length; i++) + if (keys[i] == BUTTON_RELEASED || keys[i] == BUTTON_PRESSED) + ++keys[i]; + if (glfwGetGamepadState(GLFW_JOYSTICK_1, gamepadState)) { + for (int i = 0; i < gamepadButtons.length; ++i) { + if (gamepadState.buttons(i) == GLFW_RELEASE) { + if (gamepadButtons[i] == BUTTON_RELEASED) { + gamepadButtons[i] = BUTTON_UNPRESSED; + } else if (gamepadButtons[i] != BUTTON_UNPRESSED) { + gamepadButtons[i] = BUTTON_RELEASED; + } + } else { + if (gamepadButtons[i] == BUTTON_PRESSED) { + gamepadButtons[i] = BUTTON_HELD; + } else if (gamepadButtons[i] != BUTTON_HELD) { + gamepadButtons[i] = BUTTON_PRESSED; + } + } + } + } + resized = false; + glfwPollEvents(); + } + + public void activateClearColor() { + glClearColor(0, 0, 0, 1); + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public void clear() { + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + } + + public void swap() { + glfwSwapBuffers(window); + } + + public void close() { + glfwSetWindowShouldClose(window, true); + } + + public static void terminate() { + glfwTerminate(); + } + + public boolean shouldClose() { + return glfwWindowShouldClose(window); + } + + public int keyPressed(int keyCode) { + return keys[keyCode]; + } + + public Vector3f getMouseCoords(Camera camera) { + double[] x = new double[1], y = new double[1]; + glfwGetCursorPos(window, x, y); + Vector3f ret = new Vector3f((float) x[0], (float) y[0], 0); + return ret.mul(camera.getWidth() / this.width, camera.getHeight() / this.height, 1); + } + + public int mousePressed(int button) { + return mouseButtons[button]; + } + + public boolean joystick(int joystick) { + return glfwJoystickPresent(joystick) && glfwJoystickIsGamepad(joystick); + } + + public float getJoystickAxis(int axis) { + return gamepadState.axes(axis); + } + + public int controllerButtonPressed(int button) { + return gamepadButtons[button]; + } + + public boolean wasResized() { + return resized; + } +} diff --git a/src/com/gnarwhal/ld48/engine/model/Vao.java b/src/com/gnarwhal/ld48/engine/model/Vao.java new file mode 100644 index 0000000..fa4ca4e --- /dev/null +++ b/src/com/gnarwhal/ld48/engine/model/Vao.java @@ -0,0 +1,50 @@ +package com.gnarwhal.ld48.engine.model; + +import static org.lwjgl.opengl.GL11.*; +import static org.lwjgl.opengl.GL15.*; +import static org.lwjgl.opengl.GL20.*; +import static org.lwjgl.opengl.GL30.*; + +public class Vao { + + private int numAttribs = 0; + + private int vao, ibo, count; + + private int[] vbos = new int[15]; + + public Vao(float[] vertices, int[] indices) { + vao = glGenVertexArrays(); + glBindVertexArray(vao); + addAttrib(vertices, 3); + ibo = glGenBuffers(); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW); + count = indices.length; + } + + public void addAttrib(float[] data, int size) { + int vbo = glGenBuffers(); + vbos[numAttribs] = vbo; + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, data, GL_STATIC_DRAW); + glVertexAttribPointer(numAttribs, size, GL_FLOAT, false, 0, 0); + ++numAttribs; + } + + public void render() { + glBindVertexArray(vao); + for(int i = 0; i < numAttribs; ++i) + glEnableVertexAttribArray(i); + glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_INT, 0); + for(int i = 0; i < numAttribs; ++i) + glDisableVertexAttribArray(i); + } + + public void destroy() { + for(int vbo : vbos) + glDeleteBuffers(vbo); + glDeleteBuffers(ibo); + glDeleteVertexArrays(vao); + } +} diff --git a/src/com/gnarwhal/ld48/engine/properties/ImproperFormattingException.java b/src/com/gnarwhal/ld48/engine/properties/ImproperFormattingException.java new file mode 100644 index 0000000..7aa7af8 --- /dev/null +++ b/src/com/gnarwhal/ld48/engine/properties/ImproperFormattingException.java @@ -0,0 +1,8 @@ +package com.gnarwhal.ld48.engine.properties; + +public class ImproperFormattingException extends RuntimeException { + + public ImproperFormattingException(String message) { + super(message); + } +} diff --git a/src/com/gnarwhal/ld48/engine/properties/Properties.java b/src/com/gnarwhal/ld48/engine/properties/Properties.java new file mode 100644 index 0000000..403006a --- /dev/null +++ b/src/com/gnarwhal/ld48/engine/properties/Properties.java @@ -0,0 +1,104 @@ +package com.gnarwhal.ld48.engine.properties; + +public class Properties { + + private String name; + private PropNode head, cur; + + public Properties(String name) { + this.name = new String(name); + } + + public void add(PropNode node) { + if(head == null) { + head = node; + cur = node; + } + else { + cur.next = node; + cur = cur.next; + } + } + + private PropNode get(String key) throws UndeclaredPropertyException { + String[] keys = key.split("\\."); + PropNode mobile = head; + while (mobile != null) { + if(mobile.key.equals(keys[0])) { + if(keys.length > 1 && mobile instanceof BlockNode) + return ((BlockNode) mobile).data.get(key.substring(keys[0].length() + 1)); + else + return mobile; + } + mobile = mobile.next; + } + throw new UndeclaredPropertyException("Property '" + key + "' in properties '" + name + "' was not found!"); + } + + public String getAsString(String key) throws UndeclaredPropertyException { + return ((StringNode) get(key)).data; + } + + public int getAsInt(String key) throws UndeclaredPropertyException { + return ((IntNode) get(key)).data; + } + + public int[] getAsIntArray(String key) throws UndeclaredPropertyException { + PropNode node = get(key); + if(node instanceof IntNode) + return new int[] { ((IntNode) node).data }; + return ((IntArrayNode) get(key)).data; + } + + public double getAsDouble(String key) throws UndeclaredPropertyException { + PropNode node = get(key); + if(node instanceof IntNode) + return (double) ((IntNode) node).data; + return ((DoubleNode) get(key)).data; + } + + public double[] getAsDoubleArray(String key) throws UndeclaredPropertyException { + PropNode node = get(key); + if(node instanceof DoubleNode) + return new double[] { ((DoubleNode) node).data }; + if(node instanceof IntNode) + return new double[] { ((IntNode) node).data }; + if(node instanceof IntArrayNode) { + int[] ints = getAsIntArray(key); + double[] ret = new double[ints.length]; + for (int i = 0; i < ints.length; ++i) + ret[i] = ints[i]; + return ret; + } + return ((DoubleArrayNode) get(key)).data; + } + + public static class PropNode { + public String key; + public PropNode next; + } + + public static class BlockNode extends PropNode { + public Properties data; + } + + public static class StringNode extends PropNode { + public String data; + } + + public static class IntNode extends PropNode { + public int data; + } + + public static class IntArrayNode extends PropNode { + public int[] data; + } + + public static class DoubleNode extends PropNode { + public double data; + } + + public static class DoubleArrayNode extends PropNode { + public double[] data; + } +} diff --git a/src/com/gnarwhal/ld48/engine/properties/PropertyReader.java b/src/com/gnarwhal/ld48/engine/properties/PropertyReader.java new file mode 100644 index 0000000..a763995 --- /dev/null +++ b/src/com/gnarwhal/ld48/engine/properties/PropertyReader.java @@ -0,0 +1,91 @@ +package com.gnarwhal.ld48.engine.properties; + +import com.gnarwhal.ld48.engine.properties.Properties.*; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Scanner; + +public class PropertyReader { + + private static int lineNum; + private static String path; + + public static Properties readProperties(String path) { + Properties props = null; + try { + File file = new File(path); + Scanner scanner = new Scanner(file); + PropertyReader.path = path; + lineNum = 0; + props = readBlock(file.getName(), scanner).data; + scanner.close(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + System.exit(-1); + } + return props; + } + + private static BlockNode readBlock(String name, Scanner scanner) { + BlockNode props = new BlockNode(); + props.key = name; + props.data = new Properties(name); + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + line = line.replaceAll("\\s", ""); + if(line.equals("}")) + break; + else if(line.length() < 2 || !line.substring(0, 2).equals("//")){ + String[] pair = line.split(":"); + if (pair.length != 2) + throw new ImproperFormattingException("Formatting exception on line " + line + " in file '" + path + "!"); + pair[1] = pair[1].replaceAll("\\s", ""); + if (pair[1].equals("{")) + props.data.add(readBlock(pair[0], scanner)); + else if (pair[1].matches("(\\d+|0x[\\da-f]+)")) { + IntNode node = new IntNode(); + node.key = pair[0]; + node.data = Integer.decode(pair[1]); + props.data.add(node); + } + else if (pair[1].matches("(\\d+|0x[\\d0-9]+)(,(\\d+|0x[\\d0-9]+))+")) { + String[] data = pair[1].split(","); + int[] ints = new int[data.length]; + for (int i = 0; i < ints.length; ++i) + ints[i] = Integer.decode(data[i]); + IntArrayNode node = new IntArrayNode(); + node.key = pair[0]; + node.data = ints; + props.data.add(node); + + } + else if (pair[1].matches("\\d+\\.\\d+")) { + DoubleNode node = new DoubleNode(); + node.key = pair[0]; + node.data = Double.parseDouble(pair[1]); + props.data.add(node); + } + else if (pair[1].matches("\\d+\\.\\d+(,\\d+\\.\\d+)+")) { + String[] data = pair[1].split(","); + double[] doubles = new double[data.length]; + for (int i = 0; i < doubles.length; ++i) + doubles[i] = Double.parseDouble(data[i]); + DoubleArrayNode node = new DoubleArrayNode(); + node.key = pair[0]; + node.data = doubles; + props.data.add(node); + + } + else { + StringNode node = new StringNode(); + node.key = pair[0]; + node.data = pair[1]; + props.data.add(node); + } + } + ++lineNum; + } + return props; + } +} diff --git a/src/com/gnarwhal/ld48/engine/properties/UndeclaredPropertyException.java b/src/com/gnarwhal/ld48/engine/properties/UndeclaredPropertyException.java new file mode 100644 index 0000000..6255c2e --- /dev/null +++ b/src/com/gnarwhal/ld48/engine/properties/UndeclaredPropertyException.java @@ -0,0 +1,8 @@ +package com.gnarwhal.ld48.engine.properties; + +public class UndeclaredPropertyException extends Exception { + + public UndeclaredPropertyException(String exception) { + super(exception); + } +} diff --git a/src/com/gnarwhal/ld48/engine/shaders/GradientShader.java b/src/com/gnarwhal/ld48/engine/shaders/GradientShader.java new file mode 100644 index 0000000..6b429ef --- /dev/null +++ b/src/com/gnarwhal/ld48/engine/shaders/GradientShader.java @@ -0,0 +1,22 @@ +package com.gnarwhal.ld48.engine.shaders; + +import static org.lwjgl.opengl.GL20.*; + +public class GradientShader extends Shader { + + private int color_loc; + + public GradientShader() { + super("res/shaders/gradient/vert.gls", "res/shaders/gradient/frag.gls"); + getUniforms(); + } + + @Override + protected void getUniforms() { + color_loc = glGetUniformLocation(program, "input_color"); + } + + public void setColor(float r, float g, float b, float a) { + glUniform4f(color_loc, r, g, b, a); + } +} \ No newline at end of file diff --git a/src/com/gnarwhal/ld48/engine/shaders/PlayerShader.java b/src/com/gnarwhal/ld48/engine/shaders/PlayerShader.java new file mode 100644 index 0000000..74d0935 --- /dev/null +++ b/src/com/gnarwhal/ld48/engine/shaders/PlayerShader.java @@ -0,0 +1,27 @@ +package com.gnarwhal.ld48.engine.shaders; + +import static org.lwjgl.opengl.GL20.glGetUniformLocation; +import static org.lwjgl.opengl.GL20.glUniform1f; + +public class PlayerShader extends Shader { + + private int rotation_loc; + + public PlayerShader() { + super("res/shaders/player/vert.gls", "res/shaders/player/frag.gls"); + getUniforms(); + } + + @Override + protected void getUniforms() { + rotation_loc = glGetUniformLocation(program, "rotation"); + } + + public void setRotation(float rotation) { + rotation = (rotation % 1) * 2; + if (rotation > 1) { + rotation -= 2; + } + glUniform1f(rotation_loc, rotation); + } +} \ No newline at end of file diff --git a/src/com/gnarwhal/ld48/engine/shaders/Shader.java b/src/com/gnarwhal/ld48/engine/shaders/Shader.java new file mode 100644 index 0000000..f738380 --- /dev/null +++ b/src/com/gnarwhal/ld48/engine/shaders/Shader.java @@ -0,0 +1,74 @@ +package com.gnarwhal.ld48.engine.shaders; + +import org.joml.Matrix4f; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; + +import static org.lwjgl.opengl.GL20.*; + +public abstract class Shader { + + protected int program; + protected int mvpLoc; + + protected Shader(String vertPath, String fragPath) { + program = glCreateProgram(); + + int vert = loadShader(vertPath, GL_VERTEX_SHADER); + int frag = loadShader(fragPath, GL_FRAGMENT_SHADER); + + glAttachShader(program, vert); + glAttachShader(program, frag); + + glLinkProgram(program); + + glDetachShader(program, vert); + glDetachShader(program, frag); + + glDeleteShader(vert); + glDeleteShader(frag); + + mvpLoc = glGetUniformLocation(program, "mvp"); + } + + private int loadShader(String path, int type) { + StringBuilder file = new StringBuilder(); + try { + BufferedReader reader = new BufferedReader(new FileReader(new File(path))); + String line; + while((line = reader.readLine()) != null) + file.append(line + '\n'); + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + String source = file.toString(); + int shader = glCreateShader(type); + glShaderSource(shader, source); + glCompileShader(shader); + if(glGetShaderi(shader, GL_COMPILE_STATUS) != 1) + throw new RuntimeException("Failed to compile shader: " + path + "! " + glGetShaderInfoLog(shader)); + return shader; + } + + protected abstract void getUniforms(); + + public void setMVP(Matrix4f matrix) { + glUniformMatrix4fv(mvpLoc, false, matrix.get(new float[16])); + } + + public void enable() { + glUseProgram(program); + } + + public void disable() { + glUseProgram(0); + } + + public void destroy() { + glDeleteProgram(program); + } +} diff --git a/src/com/gnarwhal/ld48/engine/texture/Texture.java b/src/com/gnarwhal/ld48/engine/texture/Texture.java new file mode 100644 index 0000000..b698298 --- /dev/null +++ b/src/com/gnarwhal/ld48/engine/texture/Texture.java @@ -0,0 +1,88 @@ +package com.gnarwhal.ld48.engine.texture; + +import org.lwjgl.BufferUtils; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; + +import static org.lwjgl.opengl.GL11.*; +import static org.lwjgl.opengl.GL13.GL_TEXTURE0; +import static org.lwjgl.opengl.GL13.glActiveTexture; + +public class Texture { + + protected int id, width, height; + + public Texture(String name) { + this(name, GL_CLAMP); + } + + public Texture(String name, int wrap) { + BufferedImage bi = null; + try { + bi = ImageIO.read(new File(name)); + } catch (IOException e) { + e.printStackTrace(); + } + if (bi != null) { + width = bi.getWidth(); + height = bi.getHeight(); + int[] pixels = bi.getRGB(0, 0, width, height, null, 0, width); + ByteBuffer buffer = BufferUtils.createByteBuffer(width * height * 4); + + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int pixel = pixels[i * width + j]; + buffer.put((byte)((pixel >> 16) & 0xFF)); // Red + buffer.put((byte)((pixel >> 8) & 0xFF)); // Green + buffer.put((byte)((pixel ) & 0xFF)); // Blue + buffer.put((byte)((pixel >> 24) & 0xFF)); // Alpha + } + } + buffer.flip(); + + id = glGenTextures(); + bind(); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, buffer); + unbind(); + } + } + + public Texture(int id, int width, int height) { + this.id = id; + this.width = width; + this.height = height; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public void bind() { + bind(0); + } + + public void bind(int activeTexture) { + glActiveTexture(GL_TEXTURE0 + activeTexture); + glBindTexture(GL_TEXTURE_2D, id); + } + + public void unbind() { + glBindTexture(GL_TEXTURE_2D, 0); + } + + public void destroy() { + glDeleteTextures(id); + } +} diff --git a/src/com/gnarwhal/ld48/game/GamePanel.java b/src/com/gnarwhal/ld48/game/GamePanel.java new file mode 100644 index 0000000..365fd28 --- /dev/null +++ b/src/com/gnarwhal/ld48/game/GamePanel.java @@ -0,0 +1,27 @@ +package com.gnarwhal.ld48.game; + +import com.gnarwhal.ld48.engine.display.Camera; +import com.gnarwhal.ld48.engine.display.Window; + +public class GamePanel { + + private Map map; + private Player player; + + public GamePanel() { + map = new Map(); + player = new Player(); + } + + public void update(Window window, Camera camera) { + player.move(window); + map.check_collisions(player); + player.update(camera); + } + + public void render(Camera camera) { + map.render_floor(camera); + player.render(camera); + map.render_walls(camera); + } +} diff --git a/src/com/gnarwhal/ld48/game/Main.java b/src/com/gnarwhal/ld48/game/Main.java new file mode 100644 index 0000000..d355508 --- /dev/null +++ b/src/com/gnarwhal/ld48/game/Main.java @@ -0,0 +1,93 @@ +package com.gnarwhal.ld48.game; + +import com.gnarwhal.ld48.engine.audio.ALManagement; +import com.gnarwhal.ld48.engine.display.Camera; +import com.gnarwhal.ld48.engine.display.Window; +import com.gnarwhal.ld48.engine.shaders.Shader; + +public class Main { + + public static int fps; + public static double dtime; + public static double adtime; + + private static double freezeDuration; + private static double freezeTime; + + private ALManagement al; + + private Window window; + private Camera camera; + + private GamePanel panel; + + public static void freeze(float duration) { + freezeDuration = duration; + freezeTime = 0; + } + + public void start() { + init(); + int frames = 0; + long curTime, pastTime, pastSec, nspf = 1000000000 / Window.REFRESH_RATE; + pastTime = System.nanoTime(); + pastSec = pastTime; + while(!window.shouldClose()) { + curTime = System.nanoTime(); + if (curTime - pastTime > nspf) { + adtime = nspf / 1000000000d; + if (freezeDuration > freezeTime + adtime) { + dtime = 0; + } else if (freezeDuration > freezeTime) { + dtime = adtime - (freezeDuration - freezeTime); + } else { + dtime = adtime; + } + freezeTime += adtime; + update(); + render(); + pastTime += nspf; + ++frames; + } + if (curTime - pastSec > 1000000000) { + fps = frames; + frames = 0; + pastSec += 1000000000; + } + if (nspf - curTime + pastTime > 10000000) try { + Thread.sleep(1); + } catch (Exception e) { + e.printStackTrace(); + } + } + al.destroy(); + Window.terminate(); + } + + private void init() { + al = new ALManagement(); + + final int WIN_WIDTH = 1920, WIN_HEIGHT = 1080; + window = new Window("Ludum Dare 48", true); + //window = new Window(WIN_WIDTH * 3/4, WIN_HEIGHT * 3/4, "Ludum Dare 48", true, true, true); + camera = new Camera(WIN_WIDTH, WIN_HEIGHT); + + panel = new GamePanel(); + } + + private void update() { + window.update(); + panel.update(window, camera); + camera.update(); + } + + private void render() { + window.clear(); + panel.render(camera); + window.swap(); + } + + public static void main(String[] args) { + new Main().start(); + } +} diff --git a/src/com/gnarwhal/ld48/game/Map.java b/src/com/gnarwhal/ld48/game/Map.java new file mode 100644 index 0000000..ed3937f --- /dev/null +++ b/src/com/gnarwhal/ld48/game/Map.java @@ -0,0 +1,381 @@ +package com.gnarwhal.ld48.game; + +import com.gnarwhal.ld48.engine.display.Camera; +import com.gnarwhal.ld48.engine.model.Vao; +import com.gnarwhal.ld48.engine.shaders.GradientShader; +import org.joml.Vector2f; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; + +public class Map { + private static class Tile { + public static abstract class RenderPass { + protected float offset; + + protected RenderPass(float offset) { + this.offset = offset; + } + + public abstract void render(Camera camera, int x, int y); + } + + public static class GradientPass extends RenderPass { + private static GradientShader shader = null; + + private Vao vao; + + private float r, g, b, a; + + public GradientPass(Vao vao, float offset, float r, float g, float b, float a) { + super(offset); + + if (shader == null) { + shader = new GradientShader(); + } + + this.vao = vao; + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } + + public void render(Camera camera, int x, int y) { + shader.enable(); + shader.setColor(r, g, b, a); + shader.setMVP(camera.getMatrix().translate(x * TILE_DIMS, y * TILE_DIMS - offset, 0).scale(TILE_DIMS, TILE_DIMS, 1)); + vao.render(); + } + } + + public static final int + LEVEL_GROUND = 0, + LEVEL_WALL = 1; + + public int level; + public boolean solid; + public ArrayList render_passes; + + public Tile(int level, boolean solid, ArrayList render_passes) { + this.level = level; + this.solid = solid; + this.render_passes = render_passes; + } + + public Tile clone() { + return new Tile( + this.level, + this.solid, + this.render_passes == null ? null : new ArrayList<>(this.render_passes) + ); + } + } + + public static float TILE_DIMS = 128.0f; + + private static final ArrayList TOP_GROUND_RENDER = new ArrayList<>(); + private static final ArrayList MID_GROUND_RENDER = new ArrayList<>(); + private static final ArrayList[] WALL_BORDER_RENDER = new ArrayList[256]; + + private static final Tile + GROUND_TILE = new Tile(Tile.LEVEL_GROUND, true, null), + WALL_TILE = new Tile(Tile.LEVEL_WALL, true, null); + + private Tile[][] map; + + public Map() { + if (TOP_GROUND_RENDER.size() == 0) { + Vao vao = new Vao( + new float[] { + 1, 0, 0, + 1, 1, 0, + 0, 1, 0, + 0, 0, 0 + }, + new int[] { + 0, 1, 3, + 1, 2, 3 + } + ); + vao.addAttrib( + new float[] { 0.5f, 0.5f, 0.5f, 0.5f }, + 1 + ); + Tile.GradientPass floor_pass = new Tile.GradientPass(vao, 0, 1, 1, 1, 1); + MID_GROUND_RENDER.add(floor_pass); + + vao = new Vao( + new float[] { + 1, 0, 0, + 1, 0.75f, 0, + 0, 0.75f, 0, + 0, 0, 0 + }, + new int[] { + 0, 1, 3, + 1, 2, 3 + } + ); + vao.addAttrib( + new float[] { 0.35f, 1.0f, 1.0f, 0.35f }, + 1 + ); + Tile.GradientPass wall_pass = new Tile.GradientPass(vao, TILE_DIMS * 0.75f, 1, 1, 1, 1); + TOP_GROUND_RENDER.add(wall_pass); + TOP_GROUND_RENDER.add(floor_pass); + + float[] vertices = new float[] { + 0, 0, 0, + 0.25f, 0, 0, + 0.75f, 0, 0, + 1, 0, 0, + 1, 0.15f, 0, + 1, 0.85f, 0, + 1, 1, 0, + 0.75f, 1, 0, + 0.25f, 1, 0, + 0, 1, 0, + 0, 0.85f, 0, + 0, 0.15f, 0, + 0.25f, 0.15f, 0, + 0.75f, 0.15f, 0, + 0.75f, 0.85f, 0, + 0.25f, 0.85f, 0 + }; + int[] indices = new int[] { + 0, 11, 12, + 0, 12, 1, + 1, 12, 2, + 12, 13, 2, + 2, 13, 3, + 3, 13, 4, + 4, 13, 5, + 13, 14, 5, + 5, 14, 6, + 6, 14, 7, + 7, 14, 8, + 14, 15, 8, + 8, 15, 9, + 9, 15, 10, + 10, 15, 11, + 15, 12, 11, + 12, 15, 13, + 13, 15, 14 + }; + + float[] values = new float[16]; + for (int i = 0; i <= 0b11111111; ++i) { + for (int j = 0; j < 16; ++j) { + values[j] = 0; + } + int mask = i; + for (int j = 0; j < 4; ++j) { + if ((mask & 1) != 0) { + for (int k = 0; k < 4; ++k) { + values[(j * 3 + k) % 12] = 0.25f; + } + } + mask >>= 1; + } + for (int j = 0; j < 4; ++j) { + if ((mask & 1) != 0) { + values[j * 3] = 0.25f; + } + mask >>= 1; + } + vao = new Vao(vertices, indices); + vao.addAttrib(values, 1); + + ArrayList render_pass = new ArrayList<>(); + render_pass.add(new Tile.GradientPass(vao, TILE_DIMS * 0.75f, 1, 1, 1, 1)); + WALL_BORDER_RENDER[i] = render_pass; + } + } + + try { + final int WALL_COLOR = 0xFF000000; + final int GROUND_COLOR = 0xFFFFFFFF; + + BufferedImage map_layout = ImageIO.read(new File("res/map/layout.png")); + map = new Tile[map_layout.getWidth()][map_layout.getHeight()]; + for (int x = 0; x < map_layout.getWidth(); ++x) { + for (int y = 0; y < map_layout.getHeight(); ++y) { + if (map_layout.getRGB(x, y) == WALL_COLOR) { + map[x][y] = WALL_TILE.clone(); + } else if (map_layout.getRGB(x, y) == GROUND_COLOR) { + map[x][y] = GROUND_TILE.clone(); + if (y > 0 && map[x][y - 1].level == Tile.LEVEL_WALL) { + map[x][y].render_passes = TOP_GROUND_RENDER; + } else { + map[x][y].render_passes = MID_GROUND_RENDER; + } + } + } + } + for (int x = 0; x < map_layout.getWidth(); ++x) { + for (int y = 0; y < map_layout.getHeight(); ++y) { + if (map[x][y].level == Tile.LEVEL_WALL) { + int result = 0; + if ( y > 0 && map[x ][y - 1].level == Tile.LEVEL_GROUND) { result |= 1 << 0; } + if (x < map.length - 1 && map[x + 1][y ].level == Tile.LEVEL_GROUND) { result |= 1 << 1; } + if ( y < map[0].length - 1 && map[x ][y + 1].level == Tile.LEVEL_GROUND) { result |= 1 << 2; } + if (x > 0 && map[x - 1][y ].level == Tile.LEVEL_GROUND) { result |= 1 << 3; } + if (x > 0 && y > 0 && map[x - 1][y - 1].level == Tile.LEVEL_GROUND) { result |= 1 << 4; } + if (x < map.length - 1 && y > 0 && map[x + 1][y - 1].level == Tile.LEVEL_GROUND) { result |= 1 << 5; } + if (x < map.length - 1 && y < map[0].length - 1 && map[x + 1][y + 1].level == Tile.LEVEL_GROUND) { result |= 1 << 6; } + if (x > 0 && y < map[0].length - 1 && map[x - 1][y + 1].level == Tile.LEVEL_GROUND) { result |= 1 << 7; } + map[x][y].render_passes = new ArrayList<>(WALL_BORDER_RENDER[result]); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void check_collisions(Player player) { + Vector2f player_motion = new Vector2f(player.velocity); + Vector2f player_position = player.base_position.add(0, 0.5f * TILE_DIMS, new Vector2f()); + + Vector2f player_pos_min = new Vector2f( + Math.min(0, player_motion.x), + Math.min(0, player_motion.y) + ).add(player_position).add(new Vector2f(-Player.PLAYER_DIMS * 0.5f)); + Vector2f player_pos_max = new Vector2f( + Math.max(0, player_motion.x), + Math.max(0, player_motion.y) + ).add(player_position).add(new Vector2f(Player.PLAYER_DIMS * 0.5f)); + + int min_x = (int) (player_pos_min.x / TILE_DIMS); + int min_y = (int) (player_pos_min.y / TILE_DIMS); + int max_x = (int) (player_pos_max.x / TILE_DIMS); + int max_y = (int) (player_pos_max.y / TILE_DIMS); + + float min_t = 0.0f; + while (min_t < 1.0f) { + player_position = player.base_position.add(0, 0.5f * TILE_DIMS, new Vector2f()); + min_t = 1.0f; + + float neutralization_direction = 0.0f; + for (int x = min_x; x <= max_x; ++x) { + for (int y = min_y; y <= max_y; ++y) { + if (map[x][y].level == Tile.LEVEL_WALL) { + Vector2f[] recall_distances = new Vector2f[] { + check_intersection(x, y, player_position.add(-Player.PLAYER_DIMS * 0.5f, -Player.PLAYER_DIMS * 0.5f, new Vector2f()), player_motion), + check_intersection(x, y, player_position.add( Player.PLAYER_DIMS * 0.5f, -Player.PLAYER_DIMS * 0.5f, new Vector2f()), player_motion), + check_intersection(x, y, player_position.add(-Player.PLAYER_DIMS * 0.5f, Player.PLAYER_DIMS * 0.5f, new Vector2f()), player_motion), + check_intersection(x, y, player_position.add( Player.PLAYER_DIMS * 0.5f, Player.PLAYER_DIMS * 0.5f, new Vector2f()), player_motion) + }; + + for (int i = 0; i < 4; ++i) { + if (recall_distances[i] != null && recall_distances[i].x < min_t) { + min_t = recall_distances[i].x; + neutralization_direction = recall_distances[i].y; + } + } + } + } + } + + if (min_t < 1.0f) { + Vector2f valid_motion = player_motion.mul(min_t, new Vector2f()); + + player.base_position.add(valid_motion); + player_motion.sub(valid_motion).mul(neutralization_direction, 1 - neutralization_direction); + + player.proc_collision(); + + if (player_motion.lengthSquared() < 0.001f) { + min_t = 1.0f; + } + } else { + player.base_position.add(player_motion); + } + } + } + + public Vector2f check_intersection(int x, int y, Vector2f position, Vector2f motion) { + Vector2f wall_start = new Vector2f(x * TILE_DIMS, y * TILE_DIMS); + Vector2f wall_end = new Vector2f(0, TILE_DIMS); + + Vector2f min = new Vector2f(1, 0); + for (int i = 0; i < 4; ++i) { + if (motion.dot(new Vector2f(-wall_end.y, wall_end.x)) < 0) { + float t0, t1; + if (motion.y == 0) { + t0 = (motion.y * (wall_start.x - position.x) / motion.x + position.y - wall_start.y) / (wall_end.y - motion.y * wall_end.x / motion.x); + t1 = (wall_end.x * t0 + wall_start.x - position.x) / motion.x; + } else { + t0 = (motion.x * (wall_start.y - position.y) / motion.y + position.x - wall_start.x) / (wall_end.x - motion.x * wall_end.y / motion.y); + t1 = (wall_end.y * t0 + wall_start.y - position.y) / motion.y; + } + + if (0 < t0 && t0 < 1 + && -0.0001f <= t1 && t1 < 1 + && t1 < min.x) { + min.set(t1, i % 2); + } + } + /* + a0 * t + b0 = c0 * s + d0 + a1 * t + b1 = c1 * s + d1 + + s = (a0 * t + b0 - d0) / c0 + a1 * t + b1 = c1 * (a0 * t + b0 - d0) / c0 + d1 + a1 * t + b1 - c1 * a0 * t / c0 = c1 * (b0 - d0) / c0 + d1 + (a1 - c1 * a0 / c0) * t + b1 = c1 * (b0 - d0) / c0 + d1 + t = (c1 * (b0 - d0) / c0 + d1 - b1) / (a1 - c1 * a0 / c0) + */ + + wall_start.add(wall_end); + wall_end.set(wall_end.y, -wall_end.x); + } + if (min.x < 1) { + return min; + } else { + return null; + } + } + + public void render_floor(Camera camera) { + int min_x = (int) Math.max((camera.getX() / TILE_DIMS), 0); + int min_y = (int) Math.max((camera.getY() / TILE_DIMS), 0); + int max_x = (int) Math.min(((camera.getX() + camera.getWidth()) / TILE_DIMS), map.length - 1); + int max_y = (int) Math.min(((camera.getY() + camera.getHeight()) / TILE_DIMS) + 1, map[0].length - 1); + for (int x = min_x; x <= max_x; ++x) { + for (int y = min_y; y <= max_y; ++y) { + if (map[x][y].level == Tile.LEVEL_GROUND) { + ArrayList render_passes = map[x][y].render_passes; + if (render_passes != null) { + for (int i = 0; i < render_passes.size(); ++i) { + render_passes.get(i).render(camera, x, y); + } + } + } + } + } + } + + public void render_walls(Camera camera) { + int min_x = (int) Math.max((camera.getX() / TILE_DIMS), 0); + int min_y = (int) Math.max(((camera.getY() + 0.75f * TILE_DIMS) / TILE_DIMS), 0); + int max_x = (int) Math.min(((camera.getX() + camera.getWidth()) / TILE_DIMS), map.length - 1); + int max_y = (int) Math.min(((camera.getY() + camera.getHeight() + 0.75f * TILE_DIMS) / TILE_DIMS), map[0].length - 1); + for (int x = min_x; x <= max_x; ++x) { + for (int y = min_y; y <= max_y; ++y) { + if (map[x][y].level == Tile.LEVEL_WALL) { + ArrayList render_passes = map[x][y].render_passes; + if (render_passes != null) { + for (int i = 0; i < render_passes.size(); ++i) { + render_passes.get(i).render(camera, x, y); + } + } + } + } + } + } +} diff --git a/src/com/gnarwhal/ld48/game/Player.java b/src/com/gnarwhal/ld48/game/Player.java new file mode 100644 index 0000000..30dfe1b --- /dev/null +++ b/src/com/gnarwhal/ld48/game/Player.java @@ -0,0 +1,419 @@ +package com.gnarwhal.ld48.game; + +import com.gnarwhal.ld48.engine.display.Camera; +import com.gnarwhal.ld48.engine.display.Window; +import com.gnarwhal.ld48.engine.model.Vao; +import com.gnarwhal.ld48.engine.shaders.PlayerShader; +import com.gnarwhal.ld48.engine.texture.Texture; +import org.joml.Vector2f; +import org.lwjgl.glfw.GLFW; + +import java.util.ArrayList; + +import static org.lwjgl.glfw.GLFW.*; + +public class Player { + private static class Particle { + public Vector2f position; + public Vector2f velocity; + public float dimensions; + + public float kill_hour; + public float clock; + } + + public static final float PLAYER_DIMS = 64.0f; + + public static final int + EXPR_NORMAL = 0, + EXPR_THREE_SMOAKS = 1, + EXPR_CONFUSED = 2, + EXPR_SQUINT = 3; + + private static final float + HOVER_FLUCTUATION = 42.0f, + HOVER_CYCLE_RATE = 2.5f; + + private static final int + NO_ACTION = 0, + QUICK_ATTACK = 1, + DASH = 2; + + public float hover_offset; + public float hover_clock; + public Vector2f base_position; + public Vector2f position; + public Vector2f velocity; + + private static PlayerShader shader; + private static Vao vao; + + private Texture body; + private Texture[] eyes; + private Texture particle; + + private int expression; + private float eye_rotation; + private float direction; + + private float rate_bias; + private float spawn_trigger; + private float position_bias; + private float target_interp_clock; + private float particle_spawn_offset; + private Vector2f particle_base_target; + private Vector2f particle_target; + private ArrayList particles; + + private int performing_action; + private float action_clock; + private float action_progress; + private boolean cancel_action; + + private float quick_attack_rotation; + private float vertical_offset; + private Vector2f quick_attack_direction; + + private Vector2f dash_direction; + + public Player() { + if (vao == null) { + shader = new PlayerShader(); + vao = new Vao( + new float[] { + 0.5f, -0.5f, 0, // Top left + 0.5f, 0.5f, 0, // Bottom left + -0.5f, 0.5f, 0, // Bottom right + -0.5f, -0.5f, 0 // Top right + }, + new int[] { + 0, 1, 3, + 1, 2, 3 + } + ); + vao.addAttrib( + new float[] { + 1, 0, + 1, 1, + 0, 1, + 0, 0 + }, + 2 + ); + } + + body = new Texture("res/img/player/body.png"); + eyes = new Texture[] { + new Texture("res/img/player/normal_eyes.png"), + new Texture("res/img/player/three_smoaks_eyes.png"), + new Texture("res/img/player/confused_eyes.png"), + new Texture("res/img/player/squint_eyes.png") + }; + particle = new Texture("res/img/player/particle.png"); + + hover_offset = 0.0f; + base_position = new Vector2f(5 * Map.TILE_DIMS + PLAYER_DIMS * 0.5f, 8 * Map.TILE_DIMS); + position = new Vector2f(base_position); + velocity = new Vector2f(); + + expression = EXPR_NORMAL; + direction = 1.0f; + + particles = new ArrayList<>(); + rate_bias = 1.0f; + spawn_trigger = 0.0f; + target_interp_clock = 1.0f; + particle_spawn_offset = 0.0f; + particle_base_target = new Vector2f(-PLAYER_DIMS, -PLAYER_DIMS * 1.5f); + particle_target = new Vector2f(particle_base_target); + } + + public float rescale(float progress, float start, float end) { + return (progress - start) / (end - start); + } + + public void move(Window window) { + final float RUN_VELOCITY = Map.TILE_DIMS * 3.5f; + final float WALK_VELOCITY = Map.TILE_DIMS * 1.5f; + final float QUICK_ATTACK_VELOCITY = Map.TILE_DIMS * 0.5f; + final float VERTICAL_VELOCITY_SCALAR = 0.75f; + + float target_velocity = RUN_VELOCITY; + if (window.keyPressed(GLFW.GLFW_KEY_LEFT_CONTROL) >= Window.BUTTON_PRESSED || window.controllerButtonPressed(GLFW_GAMEPAD_BUTTON_B) >= Window.BUTTON_PRESSED) { + target_velocity = WALK_VELOCITY; + } + if (performing_action == QUICK_ATTACK) { + target_velocity = QUICK_ATTACK_VELOCITY; + } else if (performing_action == DASH) { + target_velocity = 0.0f; + } + + Vector2f input_velocity = new Vector2f(0); + if (window.joystick(GLFW_JOYSTICK_1)) { + input_velocity.x = window.getJoystickAxis(GLFW_GAMEPAD_AXIS_LEFT_X); + input_velocity.y = window.getJoystickAxis(GLFW_GAMEPAD_AXIS_LEFT_Y); + if (Math.abs(input_velocity.x) < 0.25f) { input_velocity.x = 0; } + if (Math.abs(input_velocity.y) < 0.25f) { input_velocity.y = 0; } + } + + if (input_velocity.lengthSquared() == 0) { + if (window.keyPressed(GLFW.GLFW_KEY_A) >= Window.BUTTON_PRESSED) { + input_velocity.x -= 1; + } + if (window.keyPressed(GLFW.GLFW_KEY_D) >= Window.BUTTON_PRESSED) { + input_velocity.x += 1; + } + if (window.keyPressed(GLFW.GLFW_KEY_W) >= Window.BUTTON_PRESSED) { + input_velocity.y -= 1; + } + if (window.keyPressed(GLFW.GLFW_KEY_S) >= Window.BUTTON_PRESSED) { + input_velocity.y += 1; + } + } + + if (input_velocity.lengthSquared() != 0) { + input_velocity.normalize(target_velocity); + input_velocity.y *= VERTICAL_VELOCITY_SCALAR; + } + + if (action_clock < 0.0f) { + action_clock += Main.dtime; + } else if (performing_action == NO_ACTION) { + if (window.keyPressed(GLFW.GLFW_KEY_SPACE) == Window.BUTTON_PRESSED || window.controllerButtonPressed(GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER) == Window.BUTTON_PRESSED) { + performing_action = QUICK_ATTACK; + action_clock = 0.0f; + if (input_velocity.lengthSquared() != 0) { + quick_attack_direction = input_velocity.normalize(new Vector2f()); + } else { + quick_attack_direction = new Vector2f(direction, 0); + } + } else if (window.keyPressed(GLFW.GLFW_KEY_LEFT_SHIFT) == Window.BUTTON_PRESSED || window.controllerButtonPressed(GLFW_GAMEPAD_BUTTON_LEFT_BUMPER) == Window.BUTTON_PRESSED) { + performing_action = DASH; + action_clock = 0.0f; + if (input_velocity.lengthSquared() != 0) { + dash_direction = input_velocity.normalize(new Vector2f()); + } else { + dash_direction = new Vector2f(direction, 0); + } + } + } + + final float MAX_BIAS = 0.25f; + rate_bias = Math.max(MAX_BIAS, lerp(MAX_BIAS, 1.0f, 1 - (input_velocity.length() / RUN_VELOCITY))); + + if (performing_action == DASH) { + action_clock += Main.dtime; + + final float DASH_TIME = 0.15f; + final float DASH_COOLDOWN = 0.25f; + if (cancel_action) { + action_clock = DASH_TIME; + cancel_action = false; + } + if (action_clock >= DASH_TIME) { + performing_action = NO_ACTION; + action_clock = -DASH_COOLDOWN; + expression = EXPR_NORMAL; + rate_bias = 1.0f; + + particle_spawn_offset = 0; + } else { + rate_bias = 0.01f; + particle_spawn_offset = dash_direction.angle(new Vector2f(direction, -1)) * 2.0f / (float) Math.PI; + final float DASH_SPEED = Map.TILE_DIMS * 15; + input_velocity.set(dash_direction).mul(DASH_SPEED); + } + } + + if (performing_action == NO_ACTION && direction * input_velocity.x < 0) { + direction = -direction; + target_interp_clock = 0.0f; + } + + velocity = input_velocity.mul((float) Main.dtime); + + if (window.keyPressed(GLFW_KEY_Q) == Window.BUTTON_PRESSED) { + velocity.x = -400; + velocity.y = 0; + } + } + + public void proc_collision() {} + + private float lerp(float start, float end, float lerp) { + return start + (end - start) * lerp; + } + + public void update(Camera camera) { + camera.setCenter(base_position.x, base_position.y); + + if (performing_action == NO_ACTION) { + hover_clock = (hover_clock + (float) Main.dtime) % HOVER_CYCLE_RATE; + hover_offset = (float) Math.sin(2 * Math.PI * hover_clock * (1 / HOVER_CYCLE_RATE)) * HOVER_FLUCTUATION * 0.5f; + } + + position.set(base_position); + if (performing_action == QUICK_ATTACK) { + action_clock += Main.dtime; + + final float QUICK_ATTACK_TIME = 0.5f; + final float QUICK_ATTACK_COOLDOWN = 0.1f; + if (cancel_action) { + action_clock = QUICK_ATTACK_TIME; + cancel_action = false; + } + if (action_clock >= QUICK_ATTACK_TIME) { + performing_action = NO_ACTION; + action_clock = -QUICK_ATTACK_COOLDOWN; + expression = EXPR_NORMAL; + } + + action_progress = action_clock / QUICK_ATTACK_TIME; + + vertical_offset = 0; + quick_attack_rotation = 0; + eye_rotation = 0; + particle_spawn_offset = 0; + particle_target.set(particle_base_target); + + if (action_progress < 0.5f) { + float smooth = (1 - (float) Math.cos(Math.PI * 2 * rescale(action_progress, 0, 0.6f))) * 0.5f; + + final float RECOIL_AMOUNT = PLAYER_DIMS * 0.4f; + vertical_offset = -smooth * RECOIL_AMOUNT; + + position.y += vertical_offset; + } + + if (action_progress > 0.25f) { + float progress = rescale(action_progress, 0.15f, 1); + quick_attack_rotation = (float) Math.PI * 2 * progress * direction; + eye_rotation = progress; + particle_spawn_offset = progress * 4 * -direction; + + final float PARTICLE_BOOST = 2; + + rate_bias = 0.1f; + float sin = (float) Math.sin(quick_attack_rotation); + float cos = (float) Math.cos(quick_attack_rotation); + particle_target.set( + cos * particle_base_target.x - sin * particle_base_target.y, + sin * particle_base_target.x + cos * particle_base_target.y + ).mul(PARTICLE_BOOST); + + final float ORTHOGONAL = PLAYER_DIMS * 0.5f; + final float PARALLEL = PLAYER_DIMS * 1.15f; + + position + .add(quick_attack_direction.mul((1 - (float) Math.cos(quick_attack_rotation)) * PARALLEL, new Vector2f())) + .add(new Vector2f(-quick_attack_direction.y, quick_attack_direction.x).mul((float) Math.sin(quick_attack_rotation) * ORTHOGONAL)); + } + } + + //////// PARTICLE SYSTEM //////// + + for (int i = 0; i < particles.size(); ++i) { + Particle p = particles.get(i); + p.clock += Main.dtime; + if (p.clock >= p.kill_hour) { + particles.remove(i); + --i; + } else { + final float PARTICLE_MIN_DIMS = 16.0f; + final float PARTICLE_MAX_DIMS = 28.0f; + + float interp = p.clock / p.kill_hour; + p.dimensions = lerp(PARTICLE_MAX_DIMS, PARTICLE_MIN_DIMS, interp); + p.position.add(p.velocity.mul((float) Main.dtime, new Vector2f())); + } + } + + final float TURN_RATE = 0.7f; + target_interp_clock = Math.min(target_interp_clock + (float) Main.dtime / TURN_RATE, 1); + + spawn_trigger -= Main.dtime; + if (spawn_trigger < 0) { + Particle p = new Particle(); + + final float PARTICLE_MIN_LIFETIME = 0.5f; + final float PARTICLE_MAX_LIFETIME = 1.0f; + p.kill_hour = lerp(PARTICLE_MIN_LIFETIME, PARTICLE_MAX_LIFETIME, (float) Math.random()); + + Vector2f spawn_position = new Vector2f(position).add(0, hover_offset); + position_bias = (4.5f + direction * 0.5f + (float) Math.random() * 2 - 1 + particle_spawn_offset) % 4; + if (0 <= position_bias && position_bias < 1) { + spawn_position.add( + -PLAYER_DIMS * 0.4f * (position_bias * 2.0f - 1.0f), + -PLAYER_DIMS * 0.4f + ); + } else if (1 <= position_bias && position_bias < 2) { + position_bias -= 1; + spawn_position.add( + -PLAYER_DIMS * 0.4f, + PLAYER_DIMS * 0.4f * (position_bias * 2.0f - 1.0f) + ); + } else if (2 <= position_bias && position_bias < 3) { + position_bias -= 2; + spawn_position.add( + PLAYER_DIMS * 0.4f * (position_bias * 2.0f - 1.0f), + PLAYER_DIMS * 0.4f + ); + } else { + position_bias -= 3; + spawn_position.add( + PLAYER_DIMS * 0.4f, + -PLAYER_DIMS * 0.4f * (position_bias * 2.0f - 1.0f) + ); + } + p.position = spawn_position; + + p.velocity = new Vector2f(particle_target).mul((2 * target_interp_clock - 1) * direction, 1).add(position).sub(spawn_position).add(velocity.mul(0.1f, new Vector2f())); + + particles.add(p); + + final float BASE_SPAWN_RATE = 0.15f; + spawn_trigger = BASE_SPAWN_RATE * rate_bias; + } + } + + public void render(Camera camera) { + shader.enable(); + + particle.bind(); + for (int i = 0; i < particles.size(); ++i) { + Particle p = particles.get(i); + shader.setMVP( + camera + .getMatrix() + .translate( + p.position.x, + p.position.y, + 0 + ) + .scaleXY( + p.dimensions, + p.dimensions + ) + ); + vao.render(); + } + + shader.setMVP( + camera + .getMatrix() + .translate( + position.x, + position.y + hover_offset, + 0 + ) + .scaleXY(direction * PLAYER_DIMS, PLAYER_DIMS)); + + shader.setRotation(0); + body.bind(); + vao.render(); + + shader.setRotation(eye_rotation); + eyes[expression].bind(); + vao.render(); + } +}