From 924469a2cd5591960155513354614bd98333eef7 Mon Sep 17 00:00:00 2001 From: Joe Weaver Date: Fri, 19 Feb 2021 15:35:57 -0500 Subject: [PATCH] improved webGL shader, added label shader --- dist/builtin/shaders/label.fshader | 77 ++++++++++ dist/builtin/shaders/label.vshader | 12 ++ .../shaders/gradient_circle.fshader | 24 --- .../shaders/gradient_circle.vshader | 11 -- .../spritesheets/player_spaceship.json | 145 ------------------ .../spritesheets/player_spaceship.png | Bin 67903 -> 0 bytes src/WebGLScene.ts | 48 ------ src/Wolfie2D/AI/ControllerAI.ts | 18 --- src/Wolfie2D/AI/StateMachineAI.ts | 2 + src/Wolfie2D/DataTypes/Interfaces/AI.ts | 3 + src/Wolfie2D/DataTypes/Interfaces/Actor.ts | 3 +- src/Wolfie2D/DataTypes/Shapes/AABB.ts | 20 +++ src/Wolfie2D/DataTypes/Shapes/Circle.ts | 24 ++- src/Wolfie2D/DataTypes/Shapes/Shape.ts | 7 + src/Wolfie2D/DataTypes/Vec2.ts | 11 ++ src/Wolfie2D/Debug/Debug.ts | 30 ++++ src/Wolfie2D/Loop/Game.ts | 16 +- src/Wolfie2D/Nodes/GameNode.ts | 33 +++- src/Wolfie2D/Nodes/UIElement.ts | 10 +- src/Wolfie2D/Nodes/UIElements/Button.ts | 8 +- src/Wolfie2D/Nodes/UIElements/Label.ts | 4 + .../Registry/Registries/ShaderRegistry.ts | 10 +- src/Wolfie2D/Rendering/CanvasRenderer.ts | 16 +- .../CanvasRendering/UIElementRenderer.ts | 4 +- src/Wolfie2D/Rendering/RenderingManager.ts | 4 + src/Wolfie2D/Rendering/WebGLRenderer.ts | 94 +++++++++--- .../ShaderTypes/LabelShaderType.ts | 122 +++++++++++++++ .../ShaderTypes/SpriteShaderType.ts | 21 +-- .../ResourceManager/ResourceManager.ts | 60 ++++++-- src/Wolfie2D/Scene/Scene.ts | 5 +- src/Wolfie2D/Scene/SceneManager.ts | 22 ++- src/Wolfie2D/SceneGraph/Viewport.ts | 4 - src/Wolfie2D/Utils/Color.ts | 8 +- src/Wolfie2D/Utils/RandUtils.ts | 15 ++ src/default_scene.ts | 2 +- src/{ => demos}/Platformer.ts | 6 +- src/{ => demos}/PlatformerPlayerController.ts | 14 +- src/hw1/GradientCircleShaderType.ts | 68 -------- src/hw1/HW1_Enums.ts | 8 - src/hw1/HW1_Scene.ts | 107 ------------- src/hw1/SpaceshipPlayerController.ts | 77 ---------- src/index.html | 11 ++ src/main.ts | 40 ++--- 43 files changed, 574 insertions(+), 650 deletions(-) create mode 100644 dist/builtin/shaders/label.fshader create mode 100644 dist/builtin/shaders/label.vshader delete mode 100644 dist/hw1_assets/shaders/gradient_circle.fshader delete mode 100644 dist/hw1_assets/shaders/gradient_circle.vshader delete mode 100644 dist/hw1_assets/spritesheets/player_spaceship.json delete mode 100644 dist/hw1_assets/spritesheets/player_spaceship.png delete mode 100644 src/WebGLScene.ts delete mode 100644 src/Wolfie2D/AI/ControllerAI.ts create mode 100644 src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/LabelShaderType.ts rename src/{ => demos}/Platformer.ts (90%) rename src/{ => demos}/PlatformerPlayerController.ts (81%) delete mode 100644 src/hw1/GradientCircleShaderType.ts delete mode 100644 src/hw1/HW1_Enums.ts delete mode 100644 src/hw1/HW1_Scene.ts delete mode 100644 src/hw1/SpaceshipPlayerController.ts diff --git a/dist/builtin/shaders/label.fshader b/dist/builtin/shaders/label.fshader new file mode 100644 index 0000000..a248cba --- /dev/null +++ b/dist/builtin/shaders/label.fshader @@ -0,0 +1,77 @@ +precision mediump float; + +uniform vec4 u_BackgroundColor; +uniform vec4 u_BorderColor; +uniform float u_BorderWidth; +uniform float u_BorderRadius; +uniform vec2 u_MaxSize; + +varying vec4 v_Position; + +void main(){ + vec2 adj_MaxSize = u_MaxSize - u_BorderWidth; + vec2 rad_MaxSize = u_MaxSize - u_BorderRadius; + vec2 rad2_MaxSize = u_MaxSize - 2.0*u_BorderRadius; + + bool inX = (v_Position.x < adj_MaxSize.x) && (v_Position.x > -adj_MaxSize.x); + bool inY = (v_Position.y < adj_MaxSize.y) && (v_Position.y > -adj_MaxSize.y); + + bool inRadiusRangeX = (v_Position.x < rad_MaxSize.x) && (v_Position.x > -rad_MaxSize.x); + bool inRadiusRangeY = (v_Position.y < rad_MaxSize.y) && (v_Position.y > -rad_MaxSize.y); + + bool inRadius2RangeX = (v_Position.x < rad2_MaxSize.x) && (v_Position.x > -rad2_MaxSize.x); + bool inRadius2RangeY = (v_Position.y < rad2_MaxSize.y) && (v_Position.y > -rad2_MaxSize.y); + + if(inX && inY){ + // Inside bounds, draw background color + gl_FragColor = u_BackgroundColor; + } else { + // In boundary, draw border color + gl_FragColor = u_BorderColor; + } + + // This isn't working well right now + /* + if(inRadius2RangeX || inRadius2RangeY){ + // Draw normally + if(inX && inY){ + // Inside bounds, draw background color + gl_FragColor = u_BackgroundColor; + } else { + // In boundary, draw border color + gl_FragColor = u_BorderColor; + } + } else if(inRadiusRangeX || inRadiusRangeY){ + // Draw a rounded boundary for the inner part + float x = v_Position.x - sign(v_Position.x)*rad2_MaxSize.x; + float y = v_Position.y - sign(v_Position.y)*rad2_MaxSize.y; + + float radSq = x*x + y*y; + float bRadSq = u_BorderRadius*u_BorderRadius; + + if(radSq > bRadSq){ + // Outside of radius - draw as transparent + gl_FragColor = u_BorderColor; + } else { + gl_FragColor = u_BackgroundColor; + } + } else { + // Both coordinates are in the circular section + float x = v_Position.x - sign(v_Position.x)*rad_MaxSize.x; + float y = v_Position.y - sign(v_Position.y)*rad_MaxSize.y; + + float radSq = x*x + y*y; + float bRadSq = u_BorderRadius*u_BorderRadius; + + if(radSq > bRadSq){ + // Outside of radius - draw as transparent + gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); + } else if(sqrt(bRadSq) - sqrt(radSq) < u_BorderWidth) { + // In border + gl_FragColor = u_BorderColor; + } else { + gl_FragColor = u_BackgroundColor; + } + } + */ +} \ No newline at end of file diff --git a/dist/builtin/shaders/label.vshader b/dist/builtin/shaders/label.vshader new file mode 100644 index 0000000..093de28 --- /dev/null +++ b/dist/builtin/shaders/label.vshader @@ -0,0 +1,12 @@ +attribute vec4 a_Position; + +uniform mat4 u_Transform; + +varying vec4 v_Position; + +void main(){ + gl_Position = u_Transform * a_Position; + + // Pass position to the fragment shader + v_Position = a_Position; +} \ No newline at end of file diff --git a/dist/hw1_assets/shaders/gradient_circle.fshader b/dist/hw1_assets/shaders/gradient_circle.fshader deleted file mode 100644 index 3cd2e62..0000000 --- a/dist/hw1_assets/shaders/gradient_circle.fshader +++ /dev/null @@ -1,24 +0,0 @@ -precision mediump float; - -uniform vec4 u_Color; - -varying vec4 v_Position; - -void main(){ - // Default alpha is 0 - float alpha = 0.0; - - // Radius is 0.5, since the diameter of our quad is 1 - float radius = 0.5; - - // Get the distance squared of from (0, 0) - float dist_sq = v_Position.x*v_Position.x + v_Position.y*v_Position.y; - - if(dist_sq < radius*radius){ - // Multiply by 4, since distance squared is at most 0.25 - alpha = 4.0*dist_sq; - } - - // Use the alpha value in our color - gl_FragColor = vec4(u_Color.rgb, alpha); -} \ No newline at end of file diff --git a/dist/hw1_assets/shaders/gradient_circle.vshader b/dist/hw1_assets/shaders/gradient_circle.vshader deleted file mode 100644 index 9cb98c8..0000000 --- a/dist/hw1_assets/shaders/gradient_circle.vshader +++ /dev/null @@ -1,11 +0,0 @@ -attribute vec4 a_Position; - -uniform mat4 u_Transform; - -varying vec4 v_Position; - -void main(){ - gl_Position = u_Transform * a_Position; - - v_Position = a_Position; -} \ No newline at end of file diff --git a/dist/hw1_assets/spritesheets/player_spaceship.json b/dist/hw1_assets/spritesheets/player_spaceship.json deleted file mode 100644 index 9ae8470..0000000 --- a/dist/hw1_assets/spritesheets/player_spaceship.json +++ /dev/null @@ -1,145 +0,0 @@ -{ - "name": "player_spaceship", - "spriteSheetImage": "player_spaceship.png", - "spriteWidth": 256, - "spriteHeight": 256, - "leftBuffer": 0, - "rightBuffer": 0, - "topBuffer": 0, - "bottomBuffer": 0, - "columns": 5, - "rows": 5, - "animations": [ - { - "name": "idle", - "repeat": true, - "frames": [ - { - "index": 0, - "duration": 10 - }, - { - "index": 1, - "duration": 10 - }, - { - "index": 2, - "duration": 10 - } - ] - }, - { - "name": "boost", - "repeat": true, - "frames": [ - { - "index": 3, - "duration": 10 - }, - { - "index": 4, - "duration": 10 - }, - { - "index": 5, - "duration": 10 - } - ] - }, - { - "name": "shield", - "repeat": false, - "frames": [ - { - "index": 6, - "duration": 10 - }, - { - "index": 7, - "duration": 10 - }, - { - "index": 8, - "duration": 10 - }, - { - "index": 9, - "duration": 10 - }, - { - "index": 10, - "duration": 10 - }, - { - "index": 11, - "duration": 10 - }, - { - "index": 12, - "duration": 10 - } - ] - }, - { - "name": "explode", - "repeat": false, - "frames": [ - { - "index": 13, - "duration": 10 - }, - { - "index": 14, - "duration": 10 - }, - { - "index": 15, - "duration": 10 - }, - { - "index": 16, - "duration": 10 - }, - { - "index": 17, - "duration": 10 - }, - { - "index": 18, - "duration": 10 - }, - { - "index": 19, - "duration": 10 - }, - { - "index": 20, - "duration": 10 - }, - { - "index": 21, - "duration": 10 - }, - { - "index": 22, - "duration": 10 - }, - { - "index": 23, - "duration": 10 - } - ] - }, - { - "name": "explode", - "repeat": false, - "onEnd": "dead", - "frames": [ - { - "index": 24, - "duration": 1 - } - ] - } - ] -} \ No newline at end of file diff --git a/dist/hw1_assets/spritesheets/player_spaceship.png b/dist/hw1_assets/spritesheets/player_spaceship.png deleted file mode 100644 index 4b0f48267ad445bfd11856053df2f92a3d2489b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67903 zcmce;c{r49A2&WjmLhW7vy4y_B8se|BB?AblB^ZlFl7xfqp~Mk$QDIX3Q2ZGx$SGp zzKmt;%P@m6X3X`TQ}=y8dG6=^9lyU`$I)rVbzawbUf=EfE}w6qrp5+4ckJE)gTZzh zp3*xHgRy{*EHHLf@Jk2}k-;yx+j)cIu-rD$85m3gW~g`U;{BBQAwMTka_ST%@6@^Y zQ`)APx6X5CiiTfuI{nnCS!C#_+L)c|r-Xa?LL)ABy{h;0ig7+*Yw>o6*6ruKSH=C-D&? z9gpPX)&6)yM>vhy=hqaT9hhHR#B|BR4&cp~rz~N* zZ2#PvE=UP}#3b=+irt}qCf9<=yxp^BM3n5*woDdb(fu{K`Whb${_A7-IoP&epX5)% zO`~P6(-PTW`Meai6-fwV^K&~j;lD3(`p>uXBn6nA;Fd=teqWvG*8>GIVA;Ac@izS! zxMtMv@BGjdcJW<~Z#bbHHgE8g3gA^33+{ReWB+yU<%r+cj3Tlrx35tq3B$#dt?i>`(MJc&-U)cmWm$zH*ErMbGSvjFxw%vyxAkB z^w(dncxBkI$Tk(go(KQK9l$^y78YIoSB2}Htem}}zh4;D|G(@CujYs}zLSUZ160mt z{e2;Wi~pM^9m=-`&^6*G05?^)%oqK?%$L+JoCX_uzGXu^f-JhtdxfF2+UxLVk-u-1 zCx|KiJhgs}LjV61zbleC&1eG)R=@zqJ%39ELr|rU{r;-WT>_J*z2z~)EoA>{3Mx6;E&g@6N+rBjkhR*;tmYI;1BwoAYOK%+MoPZq2(JZ zr%=f6uMu6>UtkWS+{JE}PGyr=7>7T5^!sH7r^400?0h#8Y)BM>O-lU!3WYVh?9P;0 zVp37euiD@vJCP(NSl992r7YwjlU-nKDb35A9A78$h5pe`p}8Mfl*jV`Tfd>8a#IY3 z-$K-A0Tx|{ul+u^C@k|^#Pu%F3HAi|Z`#G6;r(;4_u2VtI0-{V=HW#`A)LAPAMImY zw-5MT8-9s92B`TZ)0j1!JfOw1En*qX|2s+lqm7pjITS-d+A=^bsv3}g=^TR%7O(!% zyC+QP%0L?SZirxJrG6(;$?t|D|M*#92(dQU!VJT2p>y<+|I5-mS)ZYx56occqAg2% zr^9Xr^nXnWo7&b9_B+R-`B-#03aR@fBJ-1$7MNi~?wkKm(%?KFOisd?ii*jv^ZuT` zQ>cOacY`IRc&FJ0F3l^@A^(o#-<-Z6sKmVGItRG@fIR-z!=#mnKsC1XqrXdAq`WEY zy@X82=RkFwX%>Z5V)1&a$Mo#**JPL}*u`IV8&J>S(sSmHyH~HzWCR^CRLaoV=Op%= z4R&fPD`a+Hc1TlBVsdCO&n-f??LW2-(ELlM4ZZxP6_;oO*Or~4{-fmg`0Y5XD%bCQ zCoq?20x{r^2{&Pv|>Cl|IYYXT*e@Hl}vo7I>w@4Jhuj<@F@N>nX`Rb7-7v zW!3fFa;Z=j_M5cuSXgb|fO@OWE`ER7Xnug{-ByKT+ks&{t>Q?p>zc9JHznHp*}%mr z)tGjF5)ON{#X|4}{w}@zrnT|?2Xa;HULA0J8#c%JJUh(d-IxaRzIWH+-<7msLf<`q zeIWPKNj})l>ux#kTL)O}zf3;qt#H;AaB2(r*VjvCd@zNWo^`ZAzc&nq-$Hzb(W?pk z+!-&{;E6g4v}$I{F+|jZcS!8MVt>R*D(Z2N)6L$K65Dt7b>v6r{;|8@BIIX zYE&<6-*T=*lfhbj&h@Y>TTEOGkYgp0x{iVTow~wK4NrFtz@B{xn(zJBSJe#I&5*;_ z$D6Yo-gp}WVkK>PPE&r-Jio4ne&J@)Qp$g=(V5QNVQNv>)(vc|br9)tKPKS*7G=^0 zo51A9_a26~NZ!8{&$GsuqOnD0ErghyN~GauEh*eST5%83FK-b5t#h!$rluXQST&tZ zN%!fwiwcQ_(#%C$3;|%LT_f$vRih8p27dJHW9ZfCx#qk|A~EUk+_sCt6d4G(FtKQ$ zCyhvOrsJ_^0V|(5bll-^3r$E^`FIO^40iaxo*0f1+BbIqZ4-MxDPOBvb)Q39rU(46 z(tfvnA2`wj3=>suS;KaoZAvkmd-%WA=FAT-;nQd*6p1GEmx6z- z4Hbabh>D1F6)E1{@F>FKFa1%_a(JkQU%irU}9i$eivZ)l(J!WFi2 zOQ>(5o471&x88|3s~S}%!rp}u74mKj`^$YF7&|aaxy2kLfXDJvg8wxtva#qsO7*5H zP)2GZued;9O!~^d>Tl-juluQhWWGjXPKry#7Nj9M-p|wGHfb0~hpD{zDFb?Ud?gb* zDpX8ZIQ|VbznCWJm$5+&qDTCd>1}Pia3mMs7acITcA_U77vXlVIyRF>z%7C0?c76z-GstC~c_6IzWY z9w|f#UT8oo5pA`CV9dXcI^alNE-1(iAuX08mp4mKS?y%+->NivaMMID+-7J6b?WQp(~o({|8H4$a)({Hy;4n* zjM1`d8&6RS_#}f)Y7K)oi2Y6?sT~?Hj`J6(gz11O2QV9ua&PQx*(cE(rTVftX1g0W zD3MQTHL7?dP~L$kXu1LEbb_8C1=YS}?)by_wN76Al^dmP$<^)Xl}A=D*mNIk(PC*? zSiE#jI-QQFcs@hyzgNtPKBq^Y%o}d8Py8HM`lA{C?jcQ0F_w0Mmx!x4OXN0cK$Dak zk%#v{tt1x zmx2KMBO_^okGQILgmo^?(FcHg^fB!^)T=|O{d4A2dyU>p-FsWuVDS=2appqVt-SeI zK>`hCILv;NA;vhgFdnfx3Jp0#p>OKaHxrK0U&1M5TULsqI7{^(z_*kP?E5j|yEjqx z@T?9D(BzkiK>#0#$PHLcQ5;ArFt7IXzuq@cIZ_$7R0!XG8?NI)Bk1Yj-!9{bo8{k` zQZ5cIEKylUKAYQ29tmrbgmM~?g?s*M#ClJ|&ns^7iHPeE0t(oS#?aXy+qRe9Bu$gR+To?F1<82W^F3H31lviUHrcJJr!7oks9 zL0*=F19QcSFp+2o2uVZ(T8t8W5|4dKJ0V51HK4bC5yh~CQ6-V2HA_iJA|6F|$(mbk z#nQQruuy9g0lo1V*SO-HvdTR+#9hczEMFXFlHbZ@T1Q_EfWkURm|Rq1NhJ>#!;)Y**1M@qM?~ z$A^2GElkK|nlbat)vSNh@gMt*<~;{!pQ75+*tkD2CFKvETRu`Zi>*+rNW=8X$a{(^ zH$?`&kaN*|g|Oc(2KPWTWQRw9)9OnDdiiEf%dyTiTQ#o^_~C9uIXTX@=g-^)_sJNVqg{x-JIJUM9X;BfG+gdd4hh< z43F$D3)%zKjz~(dlmizFH^An(u>HXk%~LK6TYUKP)zQe=Wb`KDxD4B>zZ6elu8rXu zv#G~_IP#K*k|zaqgwT4T(JJ7Wz+BkG{_@pUD%-ol5ychjL*r9aAGKn?7qSWSo?j8K zHxMhL=k{&eX-Sr)zRdCEQ23J1^e>x(yVwH1sf1OB4SsPA@7zc}OuI^gmw5LaS`>xB zE^V=s#FK1xb+kID*lm2a5H*+c-FQyi9I324$bR`5ibeEZzyG)^c7G`|;s=*j{vDI) zV%RYo?Trm(60F+(UtEF?yo5dfB!BSM0#^*RH9fD`-?w}gt4TAoL|3}OODqF29_wuH z$DH%PBh`tK@1&r{X!J!fipf)2#%u%H6^}#%zW{&s{fsktVQy$p)z0uBMa-<@ar&?a z(URcF8_$&eUtWSbOy!vaHLqb-$Or2UdpOCo)bwft_Y~*j-7|E@u1CD|o`=So+upu< zT^?Wx{-dBj1<_S38|+UKngRRcrsiJ-(@4g(r-7QJsXhkf@n8_X&JwNnF?mI`@-tE z5S=54bfJk#-=tN9@~<9tgXs++FgqL2DnG6M*3tIh4vcV!=Ds09pbao*8{O4!-<4bX zi)9tkeJHIaBTm88fgOaRecS0d2DY~T3vmfO(wB`SF8og!hh~9zs1U-nL&!R+?eJZn zwKZm|n9u(075;}3YQiklDf-^<;0RQt1{;R_$B4J9?G7vH4N=(ef>SYy9r^AlEl(2a z2>S1F608toOU)w(4!AI#A#A46T5AGuppFW2V3oXq}A)$Uou?@sB@ z(r zRHI^K=fJ=U^@f_u;UUH82;IyhrdgGH^tM+>&HCRPmPpzB`nviY3mz#-32tpbA0=`p zM?-h<2vsR)v;nIQ(tiO5IBr8jMSeF11WR$w!}X87iE3!IOVh+ZytHE$6PyRUHrlrD zBJVh>d+C4d!~mWT^H(IMV@sK1^lPcLC_l|r<<7gP6VH|mC?ckN|0i$IBvaVo%SkyS z&%_?EnG@kLTjDi*1e0A*${Mahf73C&K`lVCZi?+QoOz!FM3ID)gemPH@7UOY1}TKI zC+Q@8`X>Xp(o5y~7u3KJ6YQF1sa8+2EfjmIz3bp}@2W$}+_12d^ntus5lXO81DfjL z3Ot7G6cpCj1yErRw9wNSK}!y0vO8Ju{;c4#!tsj?%Up$Gau~COJrAM@2GeQ||*E33$mEJ#K+A**LMg)Qlp&xdGqdg546%BTA8|ZL$4Q_n>RM`{*q{xQWUEFNL z0^7TVFTNm4l-~c<>N~-^g}GyK7CAh%lFN&EuqCK1I1X20y-@c&*a(y85uRJVfhcmi zuy5k<1jI9cz3EOkjZNkM$H#MKrm(a734;DHO+Ni<0cu#f3~{WuN=%)0j7Yy!{Y*^^ zgm;es5uVc606WG05H58GkJSe5XEd}1 z+)sV_09YOPPLVWoZZQmNLxB%r&E&;=JVb6eKI@QaX8Am}@ly_J+YJ-?MLj)wIkb#6 ztaM7B-QtZp0B0FV?Q+KRDPX?Y2l zFYJlZRoy~N?^m$PaEk{b9bICgK3M8rZz+j&UBIi~f-vtHSznf2)+rbd57eyjTuH{zCjT6Q zm#;+YA#PE9>=KcdJN+;li1n6*Im(9%-tGj((LtClnE=O!(x||>!~FN?_zL()!@zrOi)lmT(42R%sL4uO7;h9B{WI9vIx>uif~h?CxPyi1Oa9jue{C9D3JlG|cUr#@ALM4Fw(r0QG*XQ)YQoi4TgYCr zCI7dJ+~xe$<&0e30Y95&h1-A}|9cfQSmi%y!{L!<+#KF+Bft|KSZBLP9Y&9GLwb68 z-5+Q=(vs~urEW(32gnRE9)ZT7`zjuEl2KIoN`&Rwo zL_PXu^v}2xj3am)#qa8bU;5Bo1+`La6$-N_YN#E8_RR}Qm}FIe{l7|QRyxtHL+Rbk z6ODavuQlm2VIk_XoZ_(3BDDlDF5k@BR&<|i_+#_aTBR5U`pJFU=V;1U(vijEBUr1;Jw%}?RdT>bYOB;j zxOSc#`Onc|)*AzZ10sdCgMmnb$N?0SQUvZ!r*}jwR8~UqS`;ZdTfOZnMR2=+swPyJjP>I$1T!rrkZsBXhZG?H1_n0;{q!btZNcM(IlZ#@GnJ<1 zjD;<%S2_;K6fLG1X%DxO)^U}s$EdZ(=;w|w+#oe(mgwFR>u54zZq(MPg&Wp8QI3X6>*fYEE3~<@CUB#v`@R6hNiIQ7Ri;7S2wHLy zI=b6f*NbkXy`L#&{RW{c@>ZJnm|K9@!QGLN*y85cyj=Rb6v|>s`yryOpbXSj<<>41 z;slIKA}(KW$}3iGK)>?d{i!oMc5|^@45glE))vFmtndy>a*NW21&8{kU@)@@x;)1Q zO>NojAc0n1tbG&voG!k-kauObYGs2NehS@DB>`m@vzx(+}XrrR^(Y?d6~#}wDOqI zu|y~sbx<+bkIuHBl771)Z^%<0sX*bTmU|%p4-=`YBCk6f+yv9TL6yQ&HH zID(#eZ2p4UJ)Of62eCa$G9@{$<~draY+Zs=pNwkmXgdvfqaLgJ+p_jPQQUkP5F{@c z=$8n}wk!ueW$bM$hB-XAu3O9C-n{o7^4^itX z+oS%fE=TbY%yNZi1zIXmgXYX^?;2+8NCJgWpKpSn=5XOgPy6CcZ(H{@VQUQKuYwj! z=jG|nlGcvB^GDhQ;W=N|SO~n}TK3t?;d$E^AQeEm1kv{l7jrs zbM3&KE|sJ2y9mMgepNBj?`NeBZOt)@OrYHqp zbPdwwwHeRp6`c619HsuUqwC2g95#{ut0bsSsZmQvXtrUy@DXJvwPRrUV|&=&6<-M{ ze-?!>tN8?wZ~p}%kj2!snMVvbN&a^gP}f*!Sm24@UkP1U_F=-3c|^_CJ~qZCVKCDZ z*O$+Xt0sr+sb+kQ;x&Ho=p%~p5nS`;D}DI)mtTJZqn)aTO&^=XbuQh1{`C8Ypo$pJ6c{nrs+94o5+UgFZowqoryp5N~WpIh|a%pF)`fLs2b{CHov zu*Lw@fFB7YTcnuI7=1G}VR+rxV(y`<>0k8Ce^NXx`1o2n*WY!61G&~;2-UsYd3e;- z@5wId@G&p^4LwMI_)1{&p(*E&zcC`Z`j7cH`}t{KO*sqyfr31mFk9g`eyBs|?9O%n>|sjZ z_Dgn4i3eZsN29&!{|JOhXqdqB&(WISHRs)!?aN3dC+O+f-?Dzw!i`OM$3D$2%lZ^c z>J+hUy+&!cWY8z~lVnMsMP|+QRb6<2x{+}KA9*9tSsYi))1ICb!Z4*DXRNK>SCM&xUm%9l*WBKu{Hie;t0xzKc=wmKG`>?BR6LMe#3FmV zOJzKX<8H<5z%`W}n5M^{H&i%b`69nwBQb0`N=^gPaSQmy!oDv1>cl~(Y?OEnCXu7V z=@)&|^;A7udP|IS#hqHhA_nZ4e<_No(tyVJ*P{9&XO_;+AHr{LSsMMlqp`(jA3vC$ z6x_9|O;IYBvhvPxaFc_g#?3&Uv@Czo(#53y@~&HWFHSdaJnqY+4O|-B7PDhEq*((x zH@V(?U0fIG4o~_EL-0)ZMDP}$W^iMBBu-mfEVJKO4cre!ODNdlF#2{8Nni-gz87X%FV(4d# z{d!uU{w@jAu;b>h#*%PW{{AQYH(kP2xJMd8PmJTSK7R>Mo0GpR~_K+0%oXrImY zb^M=u80s3Uz5{Gj{I883>tsr}Ag}aMSHS(q_|e)O$zd;bqcT|zn(HWIg-)H93qNSoZa3wK3iKH*}eaXd-BL3`8{t-ftpj&8lC? zb6~vxlK&|EW6j!swrR`Qbri%-LW>=bt)|Pm)@39Mrs&-n=S((+=5`CZ1$;Sa!B_Zz zh&zl(FWx1jN-G{R!&C-h18NsDj7JsQ%Ifs#;RXN-m;Z=%lngi3{w1chr&a7#-#@Aw z3KhJ>?tUj>f#hnGJN%$xrsySozwm5YSV`T7X{N`Hi(|H26a-Sq^K0?0>6t;^=%rEY zca<7f$1D?n54UnFLP^hh4>Vn_tl@f~+s9xiNXZ>8@Rx{`7oUJ-lLJSyKA{5Dz3AVGS_pW27-6KnDsy4kpo z&^;X-FKd){t10VftH^&M=Y_iu2}XNJN*4Iy3qwNW6}w_`J+l~AzS7IaXFSItFSvCY zRv3k&A?^jOtq&!5V=%XW;aijpUJN1qeX?F?&$=(|Ts3J7%(YbRvW_LWewe|PF8cJe z+d&sIlqANG7nPihKYES{s7y0GGzx2j%0x76gv8YMZbnwgG5+;vE;d^A#I!V3q$JMV znOPsub}c(c`?_T6vBo3jo?xik8 zQU~`*8V&epIayfo2$`8WlGAR{cKGk1aM$t&kdCpX>xT)wssVQMED*@%KWPXp&-Dg| zqBCzks^>!7gPI-o2E@dD+l+M#ap`U!JxDJl5xlT*=y4ett_WYq<;~l&HeO^nkWDHDNCdx*@84xhGAGMZ*TgPOQI6ADZ|;vi`x&sz?~v_Mn-^ThKCT_l+X^vHO$Hn7|a zhyj^5Pa*5^DciHp{mdj_eT<|gC^Sxtym+d6F}ZxYOF6SEQ@yH)o2&87RQnNsy8Xs! z$L6&;JSjF)0`~mi`+-)zWHpx<%TFp1X{h0 z)|Y}c>|<(bX}BfaOyX4@9&g9@-P5qawcf=JE?w-^YEliMa;#Fp2;3tIwZy9ibW^ z{WluGkrJoqGI5UP7H^;idneI&2e-MQqkiwE)-9e6K0M-zLlo5H#S{b0jvT0tKcf$|D9y7mAjU26 zQ^(wGD9HQsOFUfkfE6qC-RP7L(W*2TzRiPK={eQ}>A}p9R;6vP>_M2W0^9 z8j-Y(&?2&thW&GzIWJ3f7{_khw`;^O?^lN87)BEA?sZ`INBMWYL2gj9k59^NupC zLm4%{@mvOrX#}+GBo&9E`R8hu@Sc*s zLZ6{C&lZ5XwQ;pd4@YT&Zk0GRQ~sXq zb9q25Rt}snmE$kGtl^(so6zr#t3Sn>=(nLa=~Vpu>&mKTuqPG)sLfbsqC7Vnq&@}a zW{3ai-P!TfZKNG!;*9W!>|FUU3^};H`9mx8@$HgYFLLh*zXUz$5_X}zkYJe$zyFf$ zg!&%=M+fz*J;{~ID)KjSk)7_A22)qOX>QnO^_~?tcj{vFcjYotMjauRBkf4gcFmNp zX}4;z@5?{HF4Q)5_cgvr3UihGMpBei7xFc_KH?WIwi9%ux{R~e`PG+Fqy14NL)pD-3B<^9#sm*P;q0({A?e6}b zPjcOvjTA*=ED{rRW};1UIYUAk)?VgBP`%4xsbje5hW({!%bu)zppJR`pD*LD4S)wK z=l2e|;-o2FM@ids=%u!*E*!H=(omNHn_VDFD>P2eqk6vCm5+Y!F%JPKOaN|!?H`D+ zryJJ>x47m5{K&@|13GX%5%>J*{k9K2?e{&gc_D(=3pMrhdn+m3(;S$?Bb(0Igcv7b z>P7lX&$rHW1{DryY_uL@l@QT60j~#^c{dW6fCqLTsnZ_jqbPU$#qmFfe7`Qblfa2> zPpUIALW>MhqP0{!&*dt#G7eLB&kv|scK6{k0=_pa%Q!bDC77djVqaE+Lqfe4n>GKs z6?*hqe`eZBPAv6KvIf&opdKn=4<%1k0~WV^4*#;5jS=j;tKD2ECR;AOj5}DBO$%C4 zFXFb2^DdqBy@&Zif15QhuA_*t|4KwMoSXuhhPSBka)@geU~7 z=V#I^?{4Nd36`kA1YA2xA&dA3b`SCXbxlK~M;Y9^onjC##KZ|aDgodM;Fj!QYp#Ou z!BkyFv%-Qtn*=z#!4IS1g8!NQx^27=EBjCU^;Cf5U@*|vQ5~k_yl&g80a)9>U@cMPisj^zt-+gi z4Zb6sLLFaGH?0$EAWCKorypXY7?%Q5x{Mri5_pwF| z63>75F?H70;XDk0Qb0wegK0q{fOynG?Io36x0HfZ?7@TRq$Pp-dD}CNbA@x;S~7Hd zzkRl0rM|JhG^zhsQex$hgXD&1s;)O5d$+sikyhrDJOJehi0#3ErY|gErEANM>N5wy znJ9;jKvNS*AwPoQHb3K${nPT*0`|P{ruSQo|^1-pg%M)FiySH@so=I*Cr`nDH zz<`2i&vo0Fmk$@hSQ#f?X``QHfd=Q&JpMw&aaav3z$prif9(NNJFsVdn|Ao37S=&& zo?GceB8BWp18(;b57;*zc4uQ_(E}?xJg%~o$xpJbSO$?&%pHR!eClVZ?ZCLHYf_$p zki;B&b)wSl`;zb5o|*+<=guau*}tBq1*fLpIHdL*r<^kNJ`Jy^>f%Kt-1QfqsZ80of8t4Cr!?ne6lfQ}J*1|JbV_Zz{Si?7}5_d-IW*XFm7r^3k^^ z6z3dnPTcU?(;+NL`59I&?|}11_ZVVuOa1whwn=-iS0gKZNXr{hxZJ%nQJ-`7=vsq7 z>9k(v+gp0SzdzhZekPqNimUm&Cvd3y*1d-QNqf*&Bel^KVIH2D%j#tAChWL9 zC_xQtDpoFcUF2X>27q+GPAP7$uv2fKWo*fg06gvi;8TM-B~PJ+BEZz?4y*Nh|B1_b z$WoSxN&T+ItoR4#Kcb050w;yvVsi;?S}rGMJ@-1}PNi1%(Cc079>B8P6wP$>{$oVm zE%H=GcG2pW6RxY1JaXB~gf8a(RS7W1|2kFuhIslt9%05= zL<8cpEZa^PLbKHw^J)CKBSfm6hLwh;QhomS26leieU78Y9KA+ zv}+TztW7Si$_GRw;=9}J=N<(!{^@0|(==9H)3sjY(@QXz3Kx~v;~Ii?#}v(IG75PTrP)eu~m*sw#v}k+S^RE zZj>06TkV@+r**Y1C={2P&@zb4&}p8f5?s}?<`UaGiE%*k`}EYWRzIiRuBb1ue-33~ zOF{X?EQo?!q>X6P1$kyFC?z3$v1n{A_9&2sqX#>Ci|+Nb<8d%xZmVI7Uv0-9p=SHy zsjXzJBEzA5u9Y%SmZ?DV4oqbac@X78!ECR{Bgx9ZbI%?w8{ZvS`Y~gI4%pueR9#t) z)G4E_tgVt_cv5WfDRPR^No<5lzE6y3-y`a7FQB0_bY{LJR6h8cdv0;~x13 z&h@PDG-mk$2U@aIWp(}>!7tV2#TWYi#Dx7biMKYhbRrH1B5#QADt__yMr0c|mK;XX zbi1$nNS4GDC_vYQMH44yeobU+ab`>! zWJEesWX$oAd*tg!mkhdUThQOfMbD84bX3yu>b^6+6Wy%?5jc(}q%|_%_z8{8f910}YFtv_nd>tzBJ+O;tD-I43Qze4ceM96*!8|LiZ$7cE-NKWP@^TmZ z088q&LOELPs5`Sd+kzoSfVrbr6aMQBqvqCFTe|vEu#qGlJ5y!2?!l~4Tz_w^b2xrt zG8913or!6U2{sJ>Lo)qT87{1}II3ktMd_%Hel6H{CF#gP&b-}eSNc8xX?*#42nXspPkez0`p>8vaQb7aMP^~E` z>FVj4=2TXG(x-?^Ph3C?Fzk+xc{uv24A*B(k`dO+d3t-vjl+g{T5I%nz+Dfmk5e@PV z`7d8_{nj9)@WWxxENT6NlsPl&mEesazoo#>P9n3^xzJYe-n!VhJJ(Xig{q6 z<4!W+y(hEx6v<6ULTX(F0znj@{9lA7Dw+a22*N*6Z*zMA4*cW^@z$JpSQg&2Vs1ua;&^NU9` zT(3AfR2`CUu-sMw%2UusTUEQG#vsUQU*g*O#bME}(W<&zb>-*&7LpX&kMrA)_T|C>W z8%`>oXrx>4SZos8Q_*k631Nei>$d6|FgeNa9B2{2;llRREzR?6)%ioF`u-nQ&y`HA z^r{y2D`@@jYV@j#TzGUM^hLeGOc~SClv?4z9b_%t#aQTDD&n*Ij*t+i;gbE|NF{r% z&0zrLVtOl7V@UK?Bt*?LSmV;}K0KH(js`G6(+B8tdTdPp}^`45E5CPe<2M$+5YY za!D9C!2MxtM4=2>U@CP(r4e-4d=ws!z=$zYx%Cj}lDrHz6FqNP>%n^VVr~2e=ogC$ z2D#?*+ErmEW?+BBS?C~$@8=g4X64M}?LLhlGpo;0kPo?zigGf;I=73Q79>B`nxkMJ?%?7A z-r>)dw5L!s=o4i9rlfa?(O=X%NGJSkLsR93+_to(J&ic>v&U!~CgqR`KkTrf7EKU_ z2^;yuRR_ufp%qh*+cWKXA>AC$g>{R@+u~7xX3rt+hEwIE_Zi>w3(Ze%%V+dink}y* zCcuW3L?$~vT@}G7_bLSkfu^+*{;t+LHKJD*iUL9_&K=RW_J{#Fr2PjVRZ-320ahXd zBOG)}Ma@Q7%=^}hSZ~-%!)FhVC6yj(Dp591nz9$))hp5bsOPZT^|*TuZHL=S3qEj$ z9)BGT**|C7rP`njg+*SedwuF3EPxwUL-`>9HkJff`|gIE{EcBBe0&ftv`-Y^w$Ju= zd|$soax1D4&0zrN`Mcs+q@xBBtVN+7@I=s(bs+rZC(zPzK4DZV{%WMJI8O$m`tyE-y9z(RUH$Af!|%#NCYUjVd`TGh&f)|Bc1T z-s`^_XIKTw0n3jZrK9Smu);q(s1h<^2}LBu>1B5!d^uArDIU+<>(_A?WZLyoaGKYV zItyj6QwJNjmz3YU>q*?tV{w+YUO30Z=sdf4<=rS&6p}Il7<9giH^Z;=p0Uhs9;cL- z727LI3+G#RusQ!gF%Lx&tSH|3=+k;ZJJ9}QXA4e4*nxd((mlsy`p=aI3`P>B-y_Br z#MKoflmDKu&56-BdX#Q(4zvlipk&{_6}x{f4SD+|f$_f(VAqT{v97lMQ5k?KN0E2aWnx+R#454LTbzg-Oq zXmp!P%}g<2N$96gBf1ZCUrLB*PB;zjt8VuD!^du3Z`^aL1_DlV*1)FyvQb}RVpUw) zS(h8rh>1mP8bT%mWylSP{B zYC-H>)P>EaOb<{u(XTjsEFaYBB)u-fZaq%5_pyD1N1K__Kk3rXJqw{A51wCKh6<;I z1|}J2sHhl*e|e~<>LAWh6%w)8#yHH-W<6sSrrBHn~WqXaQ;pteDuD4$ME1RDR_u5 zh}XfzLcosIfppiHgm@fJ3>t?OjDygS`6nU)x_xOQ^UVxy_+`C)CY9#`NfD1Q`y&R5 zXDp@_2|+_xZ3So%_rj&rZ=fSRBqSOH6Id;GMmJ}Il{r~JF8Jt)y!@|e$<*X`U=@5v z)_@V(gw%?=U5~rHj3PJ822@W*Bs21;%B7|d75`PE)Wgm6q=9jG!{VRqB5|Y$`x`wz z7Vn1QKqU|y)v6tR!GwyHIxkmSVTlu<9+*Od>ek%AL%&l#^)G+&Auc4K>wo&VpZnry zQ|%Vmr){5!cDK%fXLj{m|16<&uv5$He7W)V>TQd>Jq1LxA919~1a-tk#ESBSpGSB$ zr!lTCRhi-4g$$9rZtJ2OGdvf{V>`B!V2+}t|)^Ejb!kT#<@@lg@|VTM4ZzHr*u zyzxo$y9zmZSc=H!{#Oi{(oVZaw0#eXBA845Za5ZxSf!W;^gRa`=7+@QR$ZuvjP>+h z?1nzXV>ho|TdkG|pYkHf!K#3js1P`(el8sC0_*}Kw;IXit@Gd!7pwx8tqo1&9rm|T zpoP+WCX&6T(&^v6$?QhEp>qg)OfJ^Rphv@qyC-3klY2I{ej;{8F8T#_$j2vGl5@?3 z(#EEYqz%h0a-(+yC`HG5rCpVIS&J+Ntg#1aPL?U@UY?7vCC?uvZVTeG31)PCJ(ppd z%Hv+Wq+&}Gg6W?iNA5SpT#oalP!}{{m)7zKar~+E#Hs)-^%J1Vk{TBW>CEu(M5ocR zZD=C1dE6v7v~hX4PYPGM+*Dh5NW-S{>f%Tk9+w~o+3vJvk2-#eegeFvg*a3mnTPXP zK%>fRpOGN9A(C<-^$<^V*60@p1Vjbt#%o&BC_GXaRBj{!q`{s+R(?#IJ>)jfWZ-7b z6lLQkHQ6=@wgcVnOipsJAVJI<#q*U`dp2#xq2YhGlh3kPHtz2)KFvt zh;Gap(anAVul6d?OJ8f#y?oD#8yBVwacC1S;$j&o^9V)Of+QKG)@8IVeJF#ODLf$L zDO38M!<>+Z5~q+^e9@AZ-TdRhjS;e*{i=k-Z7LL(n5qCkK-C*x`|i(;@^PxJt`>7~ zS9q5%zdmqSIKDh}G+%E-Lc8lFSKvPf4o1!89 zXz(61#cA;L^*tBQyheE=6w)awq{&0&$d-Q!4aEpRgY-{vd%ha)x$Bc9x4bkW5AdHl z6cne(KB&%X*eR@>M8I@zey z?~Tn0*6&8RQ-8^bFK&hd8#C8~Y5+uYrM5X*VCQ%&o|PB4)ST?ocD=Gu^ux{P&f2@d zQnxoVA5#nDxt+7QxqWU9olt15!n#7is_tiSlR2Hmly@z|R*6#6!+xbYuAyy|Av;$#u- ztutjMxXMc)dVyKcfm*FkF@W5s&xk?OSW0J`AQvmWoVMxZ|3{&y#C`%l46ut4q^~$r z&N@!b^18LR?K};2->D*dxcY%$wWY1=Q{Ub*fs;^YTx-PC^1+_U7jOJOmw$ZP=A%QQ zaL_fcQjU?zhKFeDog0?W2zsiEhC-Ccrr8X2t*DU+nzl#Rakg~2CS=~>ipgwhfo6&k z(e3+u?eDqRFgE|<12f2??G-6Ug*D9HJN}(iqVq=0d9b%&exN6>2P*n(qUutzUsF}l zaB567F{d>aU0ovsg_gB{7lpz@IBlx-_Rj|i5< zE@26716fr84Rb$)#E*VF~~*S6NKIvh$y)Grqty6ThH_OZii@kS3AxGSt)7L z=6p{F9lUX{(KM}r%BxN6%WH3FdGlQ|(g|eB&vcHZSq_dv$;c1wA1TLhmw5J1(x4uM zzqHzoyl^e(&n6Aq+upW^MnL-I{z*p(&;Y~AE<)E-R+eWp*-Ev5F|TF#rgkrPSsKnS zBdPM9$|{NNs*uvV$)EK+-*uD#sMO(STQ`c`7aw6j{hTu2@7!0m`eFrnNK(yZD83$~ z!Lp2!Ul*0UbWopDbBZ!bR?QPnLG{aju1)$OP!rt0jiE`7J!r`qN#)f>`pG?Zi)*N+ z&0&3u@`dsE)xnR>`Tnkwk^@D4#*2g7T}YA2oDw^XSxuk)#IFn=#1w+#wvp1@9&*Z! zX9Rc|rP`VbLm~nWLs=1y6X<*AUi7YfB=~SSZT2;h$iX^Z&#q$5MWOjXUR;sxQ?Oj> zL-~G7*k?V)X+TxlYjLGW{!MYQd6Bco?Ua-rf|uHg%Fz}(=-D7;K1J)$0oOZKbXuzB zMg#JP>jd(X_l*ouC{kgyfeldA&(HFVHe^d~$tbwhj6TTWMF?&99KR}0S%=m^xseZk z9k`}S05x!IaMW*u!`G6&hN_~Lh5V>NxMSZ-`_Yf0bQu80#2k2yBLmR5I;;w^(9e5n zw6XQAFN~vKxOiW)zcvxywYJfJ%}A&8a@y*R&7iJ_)mcV4MRWx`QqKLe|z)cOl5Obzv;3@_{6`pBc)`W)H;vx-(Pm;p zjpSl`x2D%(6C3o=CU|-;*k2Rmb2&kSRNsw`hA$p#@oXSrR;^59T>U~saVOE2}J<51%6|3lcD$3yvr|HFgOM+s$%7>!CTS0I$G@6cVmn&c7|b?G0XkCeLmms?|GiTor)Hkbdom_d!#ynA5b6m!aZ+`!;YcA2+?WlH!?*H4W?M+w{Z8pz~n?Q>>h_TTZ6;RSfCfArTxEUE(G{wla@0n`U^e~dQ~PM(?!eF^)TO` znJM|x*VmIQ6%>Q6b6Its5uS)aydI?MgfN4jDn4nt3?0mlpekVyehpr$Z!H?V?)YqZ zS&}`Mzr~;#ND^x#xUT6J0Q)-M`_wbTGxtJHa68aZlA!v6z+WF$Y+pmm225lZ^3&^K zIb=lvc)$gEYB7lc2Om)Bo&i(pkR^t9s~?xJ?(}k);S~T342~E{0buk^uwB{r??7LJ zvJ=%IS(9hiky`(60R+|rz{EU=#8HBpp8UqI#DxDJC z|9toNRht*M2}ZcYZmLS>IGus35IM)1L2YWlFW;8e^*hN_@i*OUvOMDa%e-ZHT#(JZ z{7W0lL7U4%G==Mjeq4ooq_>d>w=K%8xV|&waO2?4R#Og|9F2-x06c~mUKD~^dtj)A zF@5*uvi96YH`2cP8N}?h7(~Z;A^9+=;iIsPQk-Wbq<-1Ww))vEMKHX46kH3C3z=-@ zgFFNS*JH2TJdiaq0}m7`o~J}yfdB9jUgl;fw`%>|7m14i)5@z<=o7E$y7Yx_Ocf&P z?0}H+c5}4wYpX`ks~@&pmh3cj_b$F^RtwV3Zy|NI#adnmzue2KbRegx!!)+qf-7M) zHKQ;^@I9y}q8IB}y+W`4do<*E@UZ(QUmrOU|NoH^8FVsUvUC2~shFYY$1dTO-1JJb zd~ctlCnnYL?DvDG(gVyQnb$$u5L*_cYQbv_=M8qQfl0}yd-(8asv<>`z`>urvJ2G9 z(DMyVAn(C+ga1D%5?C=4(N*bzTFz(k?=Q6pYuap5+>s7s&-L%)x~-te*}I(o<=oW^;*B)vYC5~mDDYu zC9BilydWqhOYKrop17d!1;7b>+;6jMD2+fJLXv)L0n}IPmbs+!z&}%5o14wh^3&ho zEtD&W#_oH9nCwHh+19arKgGsz6Q}H#S!-H7Kw5v*a7fA_3>iqYe|;!K1?PzEUEBDT_c=i?uy`}Blx*e*WM zN)d_7LqLkB#b)@4IpKu~iOjM#_&D;tABIIPG#t~YFw(5yY82|rS!a=c4C zmGsbqA*3db*y3ap&BUOZ2%=;lW-ydSrIb2%CEunWfe(TgP*&vSlMb&4Oo5wUfD zkgrX??#!?6LfC1gSK^6(T*k`8b{hSC#nnGkha88S>Ao1kQ8u`*Gi}L-^av4OkhxNfO(t1#&kl zu78)3iN_yYW#>1=<%Li zj=Lde;3JSXH@4FdO|d9RZ^n;^HuMfk!>0q_!@b`mUI7gO%D_dokZbzqjfGwoLF{=0 z;SR%cZ2B~g-D}b1>WCJ53Zq8x--Mu6eDN%GRLo|Lz2a-3`kG*oZr9ebmjC@bjffKA zd)g+1pRbLt(p1~N%e+11^_B`ibly^f3xAZah3GNO9lr0dSjLjf_U?wfvOXr|^%0?V zrHo3sm6)d&zXdPud_2O_A_8>1p}~fPfMq`_baw~d4(AZaaDQ9zTFRqkw__b7Sks;( z*}awM+veEfLvncvNC)FnIlH!7uv!0XhC}PbeY1?)FZUwMj=z3wi!85IZEo$xq_#@K zV6H;z%J7s7-*SGHALfVTLT$@3=yPYlG+`>Hu0FMSuWXapLF2SF3)yMgSbX#CK_Hyp z=9fFY<&=j362a<3Ddj)77w`DyB$X@WJE43Q-lPD>ksCGw4@SvFNUYT%d8DbnA^0RA zE<19b1Z2;|OX<_-=}S9^gJBP5pVQzRla}=?hXvzJ>c3(aY?8oM#QDOP(`LNUE%28CGEcX+B;pfC z5g#-ur|fIt(%h6YS6c}s-+s>hM+xUk%9aq9=@w*q8eyE~$`;b00BrT)s^2tR2~HC_ z{%1c!(bhYFHi!HP&9?F3ecVJ%*S3^B^*RRQW!j4ps@^my@B3hSe(nw>X~DSOx(A4# z@}?@Q^3Qhx`~9mu0Xb8Yd_H>&qT?r$r~2;PSoc3%SD@8ey)FW~?rY$@Aw2`{w!KLP zdMH{?I)NVE)kidvwKm`_4!QJ_>TX6A1@=EPo;y=-i8$QMb8s_ho%LJr?u=Dba{GlR z*4=kfBD$JTH<(0Rb%byFFCIOJ=D@X$x^-Q+s=}X>G+6~tE@CD0={ltY@KX^E_832cYxkJD?Fue zK{K>UIaEyCT9rGyA4Q+dM}-$17@=l(f#%uq%WtSAtztmLbmosUNp}qN_^BSSq~k7dt;h-x#~-b z6uW&x?L6>Up<&0W7+PjgaOg#ExF!rR7KC1os>c zY783xKIKY?CfRo^hT&beJJ;jiDga)~+faya1&{PUKNwA)b=;uu8Vt(LK2Tn&`IC1A zNJ4hQ=TBb%d^&-S-eGw~11@Y{a=y zsnafCxyDPw8FyXo<_l+YQCC-Ian|8Ao9PlkjUcBjpKV~Kt~9p%*K4e^ER;8akc~Tp z+{f!Yc0ZJrl_g9*R|fyK_+a&vMO|z@H$DDF%kdYs-b+0vo-5(zlPrx=TsPt7vKi9O zOF34;*ng{b_<2uFo1f4rADmC~mhIO9Pn z-3n+JwGJ-xT1362irQ0JbO4@8(EQ9FS#y}w9w7lA19jKZRYYL8#N?Is9xx)B@|b!e zA~EFm#-U8Gn?L!m6Is(7XT-IcpSDJY^rB6e1opBp8Gce(x;7zl<8FC8uB#mOHg z8?)~&>hr9;>OP*Yx;4o^ ziT{BjYy#s_y2P=vf5y+3dU(2&TLj>Rrm`$I_t_CX9f4>w^ro&_RwSY<^H{`qe8h&Y z8F!%9Mrx0#3Uo%I{NPr_jTIRwxvm0Oh`^J4Wx&ZCucO=mkWAN5W$n#>fkBIV;ffHzDKpmbKSbyOg{M_5K$Bl=cLSKm!a(5=4bJIEgtbWsN|K3PZt=2#H z?LP}0(~=B&l!qd_2V2zsE-!1`-1x)p=!*ZinK6SD*gH zOn7^}9mTz9t=0wqSnsg?JGl0wI=JIvG8V4i8WXGcH|hOW-K)8A;Tqk2r7)uv0@*Rm(y4ex~(nua{%zkr@9F#RYkPOD4`P*jPdy3)2~c zpd-p8o?_nT+j8iRhZgTrzD& zUr;E|NX(4w9Ujq}rN>~D1J0?7^esMxkjn+t!Rgn`IbIws z)j8{<$r~mvfEMeN;i9#lp~3~GmSx+l0}EWkpFC;$#(ZLu3>V%x#B^x?VmthI|HjF- zM}}l9=?`;*pXYtISxFSas&U4sx}X*8m396+cG1usr>U0X=KXNZIA!?L(Nc{_xkq=_ zVLQD?LN$3>$a`5PeCU4U;KQFOU$NV=9FFpM>MhoU75D;Fuq#- zx}N$SlhMyADcD%{iBHU0;6?#M?@NhC%m2#-=u0oQ8+mSCtiodmu2mN}^XO_joDg@x zROt2A+1uA{+^%v*MRZM~k%eY}a~2T2U@8Swd2ORelq#f3FwvGJ%MHLSMq~BqLupCn zSn(&BU`J}|OUu^S^wW&%rhOQU+^zk#QEpGeT-=*{Uf1DA&vz;@a8-08?6#rybU3N7 ziLg!MC9kI94o1D#rzJodRlk6e@klUH2cFznj18{fe~G z7>Rl#w2AA@C<6RRG##2;8FPUyvyZ{vZm1)8)C6&g=m#(G?f5(j_!f>&!8ZkTKCw&d z`;*b+v4km2>Q(>JgxLE5m@msbId#Z~mrug(nT%Q;U<{UVBTXrMS7#?Nq9$TuJ?I;< zq7niFehHY;1eY>(NU#H8L| z{D9yIgt0m*Cl*j&!r9Y;2vD$A!j&AIlBX^Qn=h;vp4jYq_PE!VfP3xCrRq%|vkLC6 z6R1!l^$fk@>zL(i4Qs%Bcy!Xx^QG(UiaIkAKpkcvt~}Xf=vD=8H7HNnnUO}$axii) z{6x8y;;y`R%Bn1~3;W`?TjuimSyO~Y-zbA9aS5f~m~$Zz65_HGP*GGM>$El!M3KLr zj!=kJhqQm+MqoSY)zri-@ASpQ5XM)DRgK?fcHF-xz5w@cp<(ej+t~@M!Sg#T5t`g& zYZT2vQwY`DPIXprVjVXYqd(5L0RoSOD-PVgbRIY*kt%xSD-_TqfA+`q@Fwu}hPBnE z;L~O&oTz}cbW%QzvblHW13NeRPWY6*)N#MzYMsIKcu85w z!1EfX_GMgPgPz8$3f!!rS%2?LA$2`vs!{DQ|1B(_s7wO&W`QrWC5rsFXnhodLeRR? z97Dc6SIYIXYamTvZduiGevi;tC%NS@h$eyd`r#)0OahLot3!MkWZ-KTB>gQ;8IZAi z_Gpp1$LvR;7K}=@VDgj!81C-ZU>GM?!cwZX7SnxkWwK4iGPOyC?(UuvaIhrNUVG5w zZ)X(sOKcxTkfDdi8Ac)yG#aYhVykXt`960i0LIo1bss5_&0=pPRcg z#Bi7Nag?cC#r{X20=%=_J;2=DPpG0UcHIc0d^AUueqkr~kVE5#jn-G_A2j-=Aa6<{ zM1hg(61fe|*X1W9X~pspUF0HZ)b2>H7`6KmT|K$!Sd0UhSOBX6O`6>KY2k*;3X8LQ zN=dy6TQ}Vm+pRCmWFw^`7XWV>uCRX*ZpIBUc!V#iPCqOj0OFo)!bO7wwmn0Ya7QNL zjQoN#gx~}^rJoOW4!OAZ;wbO&9c&6lTikuk%jgmz;2a+#gSv}`=%kq>f6X@%f z0wv8mzszC0mflt?BM^C?gRaF~U#Ys%eYpt9dSIQEu|qb`)Ei)Ml)GJ}J04DY?Qf!O z@Z6y9hEf8d&EEhB%q1Ock=VIFqpp`+uee)oa(JUnt{A=8zvn&TDjUEK>t&*>-EAY> z1zdN%hcSXKY&@LxHx_{ad~AedT0}ATUc5}9cTqN@iv>4zkrPAFpc`HD~$K6x0PDzOM~bo0sP~+7;gp+|D9YkL z6dhg{SWz~SWqzFnd~sQaOQ4^f(@J;Erz?7yQwDnW^T8Mi$CimSdM0qlh;H8L8(`(c z5OJE!aUM|qRYfa2IET#lvrk+uOjiA5NFVW_VljTE{)(MUBFT2pwZCpvk6S73Y(E^(NJ zb!>CD!@MQFT`FOUC^--OeJXU-_DA0#B|A3w7M+8G90wZmp9`z8MflPurO1?y*hUd& zNFxTSDqK_N+r9B8rsu%;&-ei6hb4CT%QDU({I%fa#TR}8ZFPCN?@_N*BX5+tBn`Jr z{r9=kAM8VfKBdUDMfZ%r@V!(jljDo&i>0UOpK@C|Pqlgw+J;2G!mrMOvcP!+8Q687 zAomOFz~=uRffg$qCtqx+{MMg22Pmk>%jAY%Ood7~z%3RKo6W?Luqd2NN!qcc`|8H<6YV{;nuxV>v<_(aSvL>2T=Z=UQFE@&tEy zYIjxnFxanVuU-{M&jKuOC~<+u>DI66p!`#Qvo<0Z`ef6s&ItwNtz9R$f1g#SgK$kr zgBDiCUX1ZY=G5N5R<5%9RnF7ZR6ZgJwNvTmM+)8~y2Sy+-e?SB5iY{pN6Mqi{^QV4< zxOs?Y>H;%W{v#8s6uZ_k^FwoFL1QLW^n{x<55RH=2In_J5V#_FG-Uv*02Ntba;Sb5 z;^rl32|a1Xe&~G|oN7ICLAg)px(o*N%;-vrvT|pq_8Fdv0=*!Zgj*W}6D;fwMXwK< zt8jI-G$`dAFb&&dezL^^a>1bSsJ4`AZtQ;Ze9Z8D`g2u=tBggSxXPHZuQP+QR>a-f z+;LNHrQ*9;8m@PwT$%4EFvHt7gY}s2)>?m2WO;MrF>5)-= zwfM1!(NhJbY>Z5_ZtgzJNfJ*r?KDe;q775%IHIe90qc|LlQ95JEpE> zBw|QuFHAhG5qcL2xcju0j_m-yy}T_$s1M?*A*?oXw1di+mRNP^>fY#s%4!g0CK`H0 zMNVvPH(3j#C!HHF@jEc7>;DNpSW7pv1=R~<-RbrzXYtST19lBEBi;0N8im~UPmmw* z*2LmfES!qB^oyNed`#w%iVa2l+vdo4RoU%c+E?oxY`i_?P1;Tno9f-3s^hN50-Fs}GRoSaMsV;K z^cWKT#x$XKO|eCl_kL%uCStGi4OH|vP44>@dPBphuG7^Dw~E*I6uPb|TG7`i@Z(he zJ1gZs`qEK%=?To*zF+ue6$BT}Eb}?yf02GuN_Y7f;rBTYonOk5>ctrkaR(bQs!zHQ zsbB(8L;ndvT+S=1^%9PRq;`ff+C(znI$=K?1`m_{PBOo0o|T0`$;lQ{?hqx{62;0jWgVv`AE%>6+DJ;(%lra=MUaJ11)dZ z7-@1w_Bz@KC%g~0P>AVbyYJ8l- z^ZEv4Ln8ZmpS<5YSF+6!L>lz7@G-T&9|H%*HoM^Sb4$OuK%@l*)5i|-Pw#dNsV|O$ zzUpUQhL37|BJ*?he@R1?4r=eHogbt{e*e&>k5Rz*ow>eRqdJ3n$NMF4Ke>qdwDx@UMvDQcxi4_U-^_TY?YW@8)zNK>gCg$2nIV z9M|d9`Z>Uv3!1dDzJursEmvUATI`Ma=jnTRXg+qw8-x|U=eYKOG%C}l8s-OaNi{U2 z2x?d!7fsE_1INfobyzuXs&##1^C^eV>TNaNfayat@X#2{$qtFW&I?@1_qX$il)ja9 zJUSE@sua4e$ZL5=L|QMf;II87h4_npeF$!KZ3P2G1{t?;tnMCpjIua6BgbXhj~G{C z5cLPd0I>ATJ)8PVWAFEUIw?&JF-i6J`wv^)AK%GTS_uYClMAc*BN;_R_@$Iq1+x~9 z+=b^24Kb*WFW&04L>TLzF01p8Qs=EkZ8f z`jgh9)_z0ppBV|de$6#lgj3$Ttx~;l@s|+Vt~yX(Si9uYgEJDfXzpUcc&eyV=|U5S z?vryHaaOZezNPe?V0b!m;uLf(NJOYg`^Z{mm$+oN<3^slDT7gUyT;D1=L{Z4*#acN zi%AGM5(rp`G0}R%Lb7p4$B>9LQ+mFgvD!SGj!ZU9O*TbSt(&kyz5DO9y>Iy6p|^a4 zd=|B){1m$vbi0}e{0u%N;w+ysUDR5qVKPQsQxz!+eFM(x6wFd?pt$O+@%cd)TAVI^ zc8UC|`Ee-_Ji&M@`#R{3?9E#*FDcl8$dk)v`PMs=u#kdC2B&T8`O>Nu&qj}i9(&(q zI}j@5{?7TLjy_?K-l*kvHgl*~DNu;Yq$dbQI}Ya%S%6kHnD<(=Y&OuV83Mw5$y-ceNHF0~9;ew?4rw z%iWz4jzCZQKC@fOLuTGea#?aZLp%4NBwvkUFW*@6Sz}zFM^Sy%O%e!yru}#$2qMjM zWEHC??1y4`0l2LeKQPC7wIaAwb@X50$beX_c%q_mh0lmS=4qyPYXTHf>IGJCLWHb6 z5c6m8Pc&aU{xSv7YX~Sk^mz&eQcfUh+u@?m>FdTRi_>6GqvF}{?rujA)!K9*p85a` z&#)vdxfzwdA&z&M`ng@5(G|?-r#o8!1E6XimoobBT||V1`K#=;Gi_O{j721|d1b)= z{Nte0D~89t6e%gMXmMe~&H3I9YJcFH-$>y#Lmcz~mq<4lY~VoBmvN=NEh`KXw2i78 z6~QR?qF;G?+y&jx&$GHX3vFh7CdT}`AtO`pn017dinyOy%7fqjbJnvDmKT5Q5wIe} z`cHR7=JS@ZH@6jrS&RIyUHwCqx$S=$ok=pqErZZ_SK65C68ib&W42N+(dXE~7y&$KaFOo3sm+=@S#O7l5JXY!$4Irq6rI26^|nEn6r9 zTa_dFRLx$1i_qDNAAe7wEKZG!In8c^4?)7(?JOVvemY{I#ztXJ7uc8vFs)=6-ME5a zI_PhHj^~rHha8 z^vZPlvd=c^wsO@Wnbm)wJKP^?I4-*p?>ap4sv&Pq;Tp!FVXpD`Is~Q&oqGLaM8JTl z#le|xnd381N^>Y?0qz^0l}qQ{xc`N0Vm+2Jrc++0_&4(SddPW2D*~c^M|pLXpMyq% zM^lU4R2AlL)7`g74CJ`8LLm(5mL$T6=|Qbbp#D(|d>6$2zzCe&NHC3?_uCemi&?EP z7+Hn|1h#z`!5`R@b6mErqPCzZ$@nUY;r7~Zf5#Ue(}?tqNgS$a*CUtYFvdI_i&OP3r)^to}P2Xkss>Cc-R|(OUT_#wfuY=g$#N^DP zYhy(tQlo+MC1UmU!;DWq9G&v24kmxNBSET!XD0Go#nTkP@d`p??3*0;i&rhKbsO73 zRg(C8=rPVr?_W`Y{!g>Ss}j{5!cuTlGUC8ooBr)_${vet*x2ZGqs1z5h|3FiUm|J3 z-o7X}7+Ax#VWa@OlINSPa_Y!tF(U?b#B`u7_@XWsL2PQ{td#ezqtM5-&ygo|MLd?r zrTl7c3}P*=TVsEHNT;rcsdDYsGWfc@^sP{Ns<-F-!YRReniM!@2Tp1o*l&~3126#o zL`TYqwZ)jZ&Esot+8Uc7GXzI(@iTpG*y=)2L^y!#$x@#zn;bqy^*<9V>!zD-x%{+~ z9~#Tv*H&$QKpUmp+T;WG=YYHvY-^C_rTjy2geZ^E@h_W7-d(SuwaI6gRc}rOY|Pb< z{m+*fsLK>Hh8TR+$3?KT@sBIR_ML93`y`d- z2jaoiRdL+4<-#@Ax7?cJk`Zi>3NZIism9IM=(r)MNGb$iOHfdAdg(aJo<9{)O%EQ! zEy7z?AX;y(HXV2YUX-FXP&%s}ocbXoPmJ7>mO2v(yO>a^S4w$96%`d9LBI%KXIVYS zgo{_awbD*y(Rcy1o+)pzWaUMMJvY?^+})7$AE+sxa`Pu&y>XU_sfD=pGHY^JM8w8) z6LzO^6Fm8P?>e@w;o}ZlOYw$Toh)kE1Ar>N;n6{?X)N^=UH#wYK6^72^^QHiSt(9k zMF;CDooomwrhc8xqf5ocxjKN{8`D6;BL84w5Q37V8pj@?mWUYRDj08Q2_%24(ICFs z1JNcJac2qT=6z)lT#(xj=p%8AHqouS$W}Xl@}HQ0&Q9(QX=KW3a9xd_N9wRsd5UQ} zsi180VK#-0Yc`B80GA_o-GT3G z%SpvwJWC>!6>%b4uz`bYEOM2RRj)<*ki%j-{f^p72{CgtxB9bL_HF7Vr#044xw zENuYDwMRCQ5e#oH>(i|*Ca$E|`fa-!k&4A1n{m^--~^kaz~+Kt$f2UAxRe3>h4Mz- zpxx48lC{QyVM>=Gmg8xUE7$hEZAxIXd!zWSF%v>zBvFF$qGrsCU0r5Y#qX{nT*W;2 z_9kaL2^ij|Vuhzgds%^JB;oC~BNA1IciWzK-sofR{kyyWcDE-iguNB--tNk!s&=%r zu4NcIEas`|O+7H^JL>r>Z)$dpThL>$$#-iVKnmRf<=JfA1^;RAn_4_S+n271etx~Z ze&lau7ot%QvM192Cf8rOIFW<&l`X(HETp1|K0j@JlEf7b!y!BiOP!?d<~l`|GZEr$ zUsI}7n=HdjUVt6omirVqYKa+Dx5*JuCuJxI+qBN=>Mp;|Jzk)zzBNP5yMb9PYbZ~> zxu1gBT|pjtGIAcXtE&i{l#XSO?7G)5d(*cWMT^9Bvr-?glTCRT_lR^5fjVZDcr})0 zcW|4RynGC@9Y#$An!&nL9`+dz?%XIflR&k0QlrCQ;|^ncZy9cJ#$MPhj|_2Q3&C(( zgSG$O>amvFXN2DwRjBQfO)pX6<~!*y#*?tN+9C~RHSo}9=0^~=F+C{LWj#)Bc5T9E zA{r&BQbc>%wr=?QI}6e(S0Yx!_ILN4o+{_z6mmkLsPnEn#Z zxeKmL2+>Bw91qzyj_MG%duuj9z=~y3yLj9<$PY)Pp9ERV7ZU(i?UqGoSYpSsgcL^e zAOp>@6q5;e_r&*kypAvZfEr$av>n1x0uI`fpwoh^xv{cokKemK)LIa+)NE^`R0)Szac zW^MX{I6t@-cXoO!kvOWX-_Dp^$Y2(-WdbYb`GLh#V8JXUZusYNbFt6n@O$U1ln~i7 zGOA|r2u~0M=E`}om~VsIS21R;zY4~fqX+)h?#lGgp8d`Tj>EdLh6X~$HiJr7W}cp+ zmhvNiIWOyeqO?a)MUR8}9p@I|=*y7%b68*%aP`Azq9|3(drdNw#`6A~+p*8L-2RI~0ry^8!CB$)L$AczVU%HfsUybpE;NETL6 zU}}HVNKnQgEj+7v5kZNau67WQgYUe%P5*r+Z^DuveO?#;dVI{2pLO=tNqWox&U+h_ ze!YpA_y+?&9!Z@5L8t*05Ue`!JZr7~IA{J-Ht30PGFq4-Tp z}u6dx^sCU0fuy+bQFI1$gXh&@Msz*S-~W=yuizZ^kLxGxx)>9+p8utv)U2XaA9 zuDl?y-1qwmcD;T_yfg~0^uZr>*$WVPcyy;OSY7%S=kgw$D*+Cw3tu<&f)K#+(?go; z`8`i{fs8L})h%?LfjE6jFRgXpOqEnA$^^XLbhU0||B&LE0$$hn-|LGgGcy1=9KmQ4 zq0{2|>e`3SNKe(R<%W-7MVc>Q4K_lYq$gT^|EnB8c==>t6<3)p-c=|IVGSzF**}3n z91lfHf21cX%e*Iku@aUjVrWogdNH{C$O$z_3%wc-*bJ!CM?hkS&R`M~Hz+KhoieVQ zGBTUQmr_FqM4-cml%)N#FW+;B04o_xIglY(CyxV2aP$8xs>jE0`8s6vC}F_%Yr5nb z%qBT9+LQ#`^#T9Aum)32jy(C_zkd%tP-S|50wx0g$^yPTAE}#Kj+x{>e9p^`q>G-? zqh?y36Ff<~7WJMgdTJ4#(S`Cosz!#y3~yCsWv>l?SbMbqj`!og<1INpY?Oz@jBizK zl}&peG)EL^$`o~aH(57lQ8@I#H~+hjAR@5zfEduxThZkG_O?~p{t3>c8??+sgos%O zlMZd8rX1Kr5NO{KJ5Zf8l{~(uzU$0>%@zAm!PZT*y9dyDn?I;vpB~@-j2+r{0K0x7 zFK$(R5~FxWSf+%Qjh%2idIC4&5x)PP8_}S>koIINQEr2en*p`FG%3?{y`DKQNy6L$ zzO-oQM?)D&N%X+6UG{vLf>!%cXv+2~e16_1WNLJB@$j0AWGOH-p;dWPjVwA|qbxiVndc#(l-D1>XO zrKL>zMM^J}x~1t2>;r*PFY~pYVAPuG-3tvUYQHJLW{;b6kQ2$;=W*OxX(?lB%a5A`z;CoYXGHxRgWcU_IQ+75{w`=yN zuu9E?0h(%RkCL2)$7X=3aqC2A;097NJ)*`j1?WR4d8|y}p5vtJm(rRVJo1+xgil{m zS!)a(yh*GFd19c4u$a+!z|rlsxxTm7Sq~5%qob@#T;ednTwGT+Y2PCxp;=`u&Bnoj z6x~y2n}EGgL?gf{R!xoTCbEq&wcOy^@09OVBnB%Y$O+z4u_S zm<)tR0i9r0h-x0+qYu>4({7^=ong;1fkf6=;ahvD*pf3JmPcG$NcHNCwozUW*GHv6 zjPT#MEyz()1|h+LgSrCRvl%gC%7gr^kx8;x_+BSlm9i?QF;ib985GEdv6s4^D^GV! zXgs^yKQ>gEab2;_psx0yXH7}Tb}UWB?^#rY7WJcxtCa-oy4O-iK&SeAH|o_ks>RbR z%|YQ&cui!)$q!wR^($Jv`}@ zY67CAsPV*$i4ol{cci#=IY<1@Sq0V6J*@8no{a35o8bNt`4Q3Xh>G{i89MUV^|0t5 zMf%INgHurvl6$isUF+q0#GqJ%hJFyw-zCwG%@&?`%mdVEq^uGB%Oah>U=)Ymqo<9h+~&O;&h5BjcvX^yY$})J>^O<;$Y$aDDQWEB)6>o2=}u zvE>F*e%*r$GrDt#Cr$;2vn8_ey3~YAR;*w62DTEq$2%q3QM=CNk9;S-Zw4i^Ox=V1 z1?blN=+z?dzI%e(tXs!#G?EHh*8-~hMF?>>b`pVu8tm?&wrC!+LWY%Xlp_vxp)+r2=`Hq=Rkx)LFP~YobwmLdXo5~!z)?>N{5D(`0!j7 z@=$;fOn-~bFgmUrUQfr7)gjluzw`^&vLH8?Y+xSK$^9i?tOJbHobuZQtIz9O9^>B0 zL{!VZPSYlP|H|Q9@w0K;SkVimMa8Y*S_+Mh+S30cj2M{v@D<$6-Y3^Rjo5kRr5iD9rOSNTAImKm*EAeWLnIos5XKh^AKiI(7dqS{4fqPje40Qt zSLHc1mbaGWCgw7f<6&9-lf>snR9VVgTZJ<0{m&dEwU{`}UAr`KG>$!)wW3I7v%ee~ z?BUdu2~<-{_P;l0oO%93;t$oF2l&GlZjAm0GAO95Drf&G(t||DqwkuG4v#b2ifxAJ ziCnJ!>GKJ-?AVK%a<-qAsp=>Z$vyRUlg;=J0q?-Jm5~V*(l-dsU_;#bzR6}7#?b=F zjtl{N4l+x zZH@H$9npq=@5zLKMEbSQ!zWXD=$}tT$nDS7@4p{cBrnFwH@MjJL8dFbzb2R=;B= z=0yOs>6EGrddZ%mrWQ=!LKV3n*V1a5km8W-zmSi%2?(x28f{EBV_8o09(H#UyG$M_oQB>x&_MvrZ4I0@{_sk&wJFIObO&R_Pp=XE23d zlKgt;sxqyxpKb70_cvl^zzNOWM2+ftth-PyocWsM8$QE*gPScxBdzlIikI=6NIN_O zNp148K()h*myOO5WL()pWoG?uc+5PfUxBa;e%@oTX;^WPjKvuZS;;^|P#*;%GC>ry z>)#!kwS8^wmUgHuktzlzmSvz2^rRcEP)^3qdF#mmkYulxyU;HUXr^$;=?0D(F^H77 znoq|*dXUu`)8RM_!xavvD`N0)x|@jWyu~-2frKb()Xdbm^}4h5zhD3lxH2BzTNf-t zhR$2R44f9Y22T6J9`>dRgYOz+#PhZCN=Sh&Qyvp%gw8OTl~bjK zP?4(LG^;{TpTAQ@6Y^8^UV;EcBd6j3J-RU2=Vk6*iB@R~{XFoD(Fl zo^5hH8INEKVE~!zmGFSVG94a<7-s2ebjNqc4;2A1s30dls2-IG(w4e*Y!sp65disp zFGJ+lV_U&T+5AuK90dM!V#;ZIH2+nA8@StM8uI+psA)*`yP#u` z5ctX1H?|i7eI6NEXd3P4>VdOg9p3mIL~)M2yCQ}9cBKy$5f9t|;jA+Y-70Fy`K=ud zyqOqo3@-K`&AS8 z4e!7p! z>)dhl@;^(Dr302+IcW47ma;<>RAPwwepJOR-c=piJWLCRi2`0guF!w=la7n@KQrQu zJPD3LV_wqO?z|`nJYR-%MEA=(ZwP{S*;-nJK0$?U6b#Z>Xs(EJ<-{0BY?ot`qtZ1D12ZIPvy445AAh?{h{Fx#nYB!z! ztwtArl>P>@E`Y}_R}lL6bf8?fEIrBpnO4QH5RAMu#vT}spW z4(1>~(7Fs(>jJwRVaO%<=SPFe<5}6)*-OjcKb=5qtuo)K-aIEfRd4-n3%b||wuh4X zH~=0ybQib$NQyBiA4`Dp{Z|Bw6PBNyVq&abUxDnz`+R>|SL%NNWri$; z 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 Bs{Q}~ diff --git a/src/WebGLScene.ts b/src/WebGLScene.ts deleted file mode 100644 index d95cb02..0000000 --- a/src/WebGLScene.ts +++ /dev/null @@ -1,48 +0,0 @@ -import Vec2 from "./Wolfie2D/DataTypes/Vec2"; -import { GraphicType } from "./Wolfie2D/Nodes/Graphics/GraphicTypes"; -import Point from "./Wolfie2D/Nodes/Graphics/Point"; -import Rect from "./Wolfie2D/Nodes/Graphics/Rect"; -import AnimatedSprite from "./Wolfie2D/Nodes/Sprites/AnimatedSprite"; -import Sprite from "./Wolfie2D/Nodes/Sprites/Sprite"; -import Scene from "./Wolfie2D/Scene/Scene"; -import Color from "./Wolfie2D/Utils/Color"; - -export default class WebGLScene extends Scene { - - private point: Point; - private rect: Rect; - private player: AnimatedSprite; - private t: number = 0; - - loadScene() { - this.load.spritesheet("player", "hw1_assets/spritesheets/player_spaceship.json"); - } - - startScene() { - this.addLayer("primary"); - - this.point = this.add.graphic(GraphicType.POINT, "primary", {position: new Vec2(100, 100), size: new Vec2(10, 10)}) - this.point.color = Color.CYAN; - console.log(this.point.color.toStringRGBA()); - - this.rect = this.add.graphic(GraphicType.RECT, "primary", {position: new Vec2(300, 100), size: new Vec2(100, 50)}); - this.rect.color = Color.ORANGE; - - this.player = this.add.animatedSprite("player", "primary"); - this.player.position.set(800, 500); - this.player.scale.set(0.5, 0.5); - this.player.animation.play("idle"); - } - - updateScene(deltaT: number) { - this.t += deltaT; - - let s = Math.sin(this.t); - let c = Math.cos(this.t); - - this.point.position.x = 100 + 100*c; - this.point.position.y = 100 + 100*s; - - this.rect.rotation = this.t; - } -} \ No newline at end of file diff --git a/src/Wolfie2D/AI/ControllerAI.ts b/src/Wolfie2D/AI/ControllerAI.ts deleted file mode 100644 index 8eb3c62..0000000 --- a/src/Wolfie2D/AI/ControllerAI.ts +++ /dev/null @@ -1,18 +0,0 @@ -import AI from "../DataTypes/Interfaces/AI"; -import GameEvent from "../Events/GameEvent"; -import GameNode from "../Nodes/GameNode"; - -/** - * A very basic AI class that just runs a function every update - */ -export default class ControllerAI implements AI { - protected owner: GameNode; - - initializeAI(owner: GameNode, options: Record): void { - this.owner = owner; - } - - handleEvent(event: GameEvent): void {} - - update(deltaT: number): void {} -} \ No newline at end of file diff --git a/src/Wolfie2D/AI/StateMachineAI.ts b/src/Wolfie2D/AI/StateMachineAI.ts index 1ee0ddf..d0cc34d 100644 --- a/src/Wolfie2D/AI/StateMachineAI.ts +++ b/src/Wolfie2D/AI/StateMachineAI.ts @@ -12,4 +12,6 @@ export default class StateMachineAI extends StateMachine implements AI { // @implemented initializeAI(owner: GameNode, config: Record): void {} + + activate(options: Record): void {} } \ No newline at end of file diff --git a/src/Wolfie2D/DataTypes/Interfaces/AI.ts b/src/Wolfie2D/DataTypes/Interfaces/AI.ts index a7572e9..33d0c86 100644 --- a/src/Wolfie2D/DataTypes/Interfaces/AI.ts +++ b/src/Wolfie2D/DataTypes/Interfaces/AI.ts @@ -10,6 +10,9 @@ export default interface AI extends Updateable { /** Initializes the AI with the actor and any additional config */ initializeAI(owner: GameNode, options: Record): void; + /** Activates this AI from a stopped state and allows variables to be passed in */ + activate(options: Record): void; + /** Handles events from the Actor */ handleEvent(event: GameEvent): void; } \ No newline at end of file diff --git a/src/Wolfie2D/DataTypes/Interfaces/Actor.ts b/src/Wolfie2D/DataTypes/Interfaces/Actor.ts index ea7947d..37dc91a 100644 --- a/src/Wolfie2D/DataTypes/Interfaces/Actor.ts +++ b/src/Wolfie2D/DataTypes/Interfaces/Actor.ts @@ -30,6 +30,7 @@ export default interface Actor { /** * Sets the AI to start/stop for this Actor. * @param active The new active status of the AI. + * @param options An object that allows options to be pased to the activated AI */ - setAIActive(active: boolean): void; + setAIActive(active: boolean, options: Record): void; } \ No newline at end of file diff --git a/src/Wolfie2D/DataTypes/Shapes/AABB.ts b/src/Wolfie2D/DataTypes/Shapes/AABB.ts index ad6fb42..d80c9db 100644 --- a/src/Wolfie2D/DataTypes/Shapes/AABB.ts +++ b/src/Wolfie2D/DataTypes/Shapes/AABB.ts @@ -23,6 +23,26 @@ export default class AABB extends Shape { this.halfSize = halfSize ? halfSize : new Vec2(0, 0); } + /** Returns a point representing the top left corner of the AABB */ + get topLeft(): Vec2 { + return new Vec2(this.left, this.top) + } + + /** Returns a point representing the top right corner of the AABB */ + get topRight(): Vec2 { + return new Vec2(this.right, this.top) + } + + /** Returns a point representing the bottom left corner of the AABB */ + get bottomLeft(): Vec2 { + return new Vec2(this.left, this.bottom) + } + + /** Returns a point representing the bottom right corner of the AABB */ + get bottomRight(): Vec2 { + return new Vec2(this.right, this.bottom) + } + // @override getBoundingRect(): AABB { return this.clone(); diff --git a/src/Wolfie2D/DataTypes/Shapes/Circle.ts b/src/Wolfie2D/DataTypes/Shapes/Circle.ts index 8cdca38..004ab79 100644 --- a/src/Wolfie2D/DataTypes/Shapes/Circle.ts +++ b/src/Wolfie2D/DataTypes/Shapes/Circle.ts @@ -7,7 +7,7 @@ import Shape from "./Shape"; */ export default class Circle extends Shape { private _center: Vec2; - private radius: number; + radius: number; /** * Creates a new Circle @@ -32,6 +32,24 @@ export default class Circle extends Shape { return new Vec2(this.radius, this.radius); } + get r(): number { + return this.radius; + } + + set r(radius: number) { + this.radius = radius; + } + + // @override + /** + * A simple boolean check of whether this AABB contains a point + * @param point The point to check + * @returns A boolean representing whether this AABB contains the specified point + */ + containsPoint(point: Vec2): boolean { + return this.center.distanceSqTo(point) <= this.radius*this.radius; + } + // @override getBoundingRect(): AABB { return new AABB(this._center.clone(), new Vec2(this.radius, this.radius)); @@ -51,4 +69,8 @@ export default class Circle extends Shape { clone(): Circle { return new Circle(this._center.clone(), this.radius); } + + toString(): string { + return "(center: " + this.center.toString() + ", radius: " + this.radius + ")"; + } } \ No newline at end of file diff --git a/src/Wolfie2D/DataTypes/Shapes/Shape.ts b/src/Wolfie2D/DataTypes/Shapes/Shape.ts index 93c00b9..04113c9 100644 --- a/src/Wolfie2D/DataTypes/Shapes/Shape.ts +++ b/src/Wolfie2D/DataTypes/Shapes/Shape.ts @@ -71,6 +71,13 @@ export default abstract class Shape { */ abstract overlaps(other: Shape): boolean; + /** + * A simple boolean check of whether this Shape contains a point + * @param point The point to check + * @returns A boolean representing whether this Shape contains the specified point + */ + abstract containsPoint(point: Vec2): boolean; + static getTimeOfCollision(A: Shape, velA: Vec2, B: Shape, velB: Vec2): [Vec2, Vec2, boolean, boolean] { if(A instanceof AABB && B instanceof AABB){ return Shape.getTimeOfCollision_AABB_AABB(A, velA, B, velB); diff --git a/src/Wolfie2D/DataTypes/Vec2.ts b/src/Wolfie2D/DataTypes/Vec2.ts index 10783a9..63d1195 100644 --- a/src/Wolfie2D/DataTypes/Vec2.ts +++ b/src/Wolfie2D/DataTypes/Vec2.ts @@ -269,6 +269,17 @@ export default class Vec2 { return this; } + /** + * Does an element wise remainder operation on this vector. this.x %= other.x and this.y %= other.y + * @param other The other vector + * @returns this vector + */ + remainder(other: Vec2): Vec2 { + this.x = this.x % other.x; + this.y = this.y % other.y; + return this; + } + /** * Returns the squared distance between this vector and another vector * @param other The vector to compute distance squared to diff --git a/src/Wolfie2D/Debug/Debug.ts b/src/Wolfie2D/Debug/Debug.ts index 7cb2090..22cfc8b 100644 --- a/src/Wolfie2D/Debug/Debug.ts +++ b/src/Wolfie2D/Debug/Debug.ts @@ -78,6 +78,36 @@ export default class Debug { this.debugRenderingContext.globalAlpha = alpha; } + /** + * Draws a circle at the specified position + * @param center The center of the circle + * @param radius The dimensions of the box + * @param filled A boolean for whether or not the circle is filled + * @param color The color of the circle + */ + static drawCircle(center: Vec2, radius: number, filled: boolean, color: Color): void { + let alpha = this.debugRenderingContext.globalAlpha; + this.debugRenderingContext.globalAlpha = color.a; + + if(filled){ + this.debugRenderingContext.fillStyle = color.toString(); + this.debugRenderingContext.beginPath(); + this.debugRenderingContext.arc(center.x, center.y, radius, 0, 2 * Math.PI); + this.debugRenderingContext.closePath(); + this.debugRenderingContext.fill(); + } else { + let lineWidth = 2; + this.debugRenderingContext.lineWidth = lineWidth; + this.debugRenderingContext.strokeStyle = color.toString(); + this.debugRenderingContext.beginPath(); + this.debugRenderingContext.arc(center.x, center.y, radius, 0, 2 * Math.PI); + this.debugRenderingContext.closePath(); + this.debugRenderingContext.stroke(); + } + + this.debugRenderingContext.globalAlpha = alpha; + } + /** * Draws a ray at the specified position * @param from The starting position of the ray diff --git a/src/Wolfie2D/Loop/Game.ts b/src/Wolfie2D/Loop/Game.ts index 47b174c..0e40f36 100644 --- a/src/Wolfie2D/Loop/Game.ts +++ b/src/Wolfie2D/Loop/Game.ts @@ -18,6 +18,7 @@ import EnvironmentInitializer from "./EnvironmentInitializer"; import Vec2 from "../DataTypes/Vec2"; import Registry from "../Registry/Registry"; import WebGLRenderer from "../Rendering/WebGLRenderer"; +import Scene from "../Scene/Scene"; /** * The main loop of the game engine. @@ -130,7 +131,7 @@ export default class Game { /** * Starts the game */ - start(): void { + start(InitialScene: new (...args: any) => Scene, options: Record): void { // Set the update function of the loop this.loop.doUpdate = (deltaT: number) => this.update(deltaT); @@ -142,7 +143,9 @@ export default class Game { // Load the items with the resource manager this.resourceManager.loadResourcesFromQueue(() => { - // When we're dont loading, start the loop + // When we're done loading, start the loop + console.log("Finished Preload - loading first scene"); + this.sceneManager.addScene(InitialScene, options); this.loop.start(); }); } @@ -178,14 +181,7 @@ export default class Game { // Clear the canvases Debug.clearCanvas(); - if(this.gameOptions.useWebGL){ - (this.ctx).clearColor(this.clearColor.r, this.clearColor.g, this.clearColor.b, this.clearColor.a); - (this.ctx).clear((this.ctx).COLOR_BUFFER_BIT | (this.ctx).DEPTH_BUFFER_BIT); - } else { - (this.ctx).clearRect(0, 0, this.WIDTH, this.HEIGHT); - (this.ctx).fillStyle = this.clearColor.toString(); - (this.ctx).fillRect(0, 0, this.WIDTH, this.HEIGHT); - } + this.renderingManager.clear(this.clearColor); this.sceneManager.render(); diff --git a/src/Wolfie2D/Nodes/GameNode.ts b/src/Wolfie2D/Nodes/GameNode.ts index 260a687..c3a0fd1 100644 --- a/src/Wolfie2D/Nodes/GameNode.ts +++ b/src/Wolfie2D/Nodes/GameNode.ts @@ -18,6 +18,7 @@ import NavigationPath from "../Pathfinding/NavigationPath"; import TweenManager from "../Rendering/Animations/TweenManager"; import Debug from "../Debug/Debug"; import Color from "../Utils/Color"; +import Circle from "../DataTypes/Shapes/Circle"; /** * The representation of an object in the game world. @@ -198,6 +199,15 @@ export default abstract class GameNode implements Positioned, Unique, Updateable this.scene.getPhysicsManager().registerObject(this); } + /** + * Sets the collider for this GameNode + * @param collider The new collider to use + */ + setCollisionShape(collider: Shape): void { + this.collisionShape = collider; + this.collisionShape.center.copy(this.position); + } + // @implemented /** * @param group The name of the group that will activate the trigger @@ -254,8 +264,9 @@ export default abstract class GameNode implements Positioned, Unique, Updateable } // @implemented - setAIActive(active: boolean): void { + setAIActive(active: boolean, options: Record): void { this.aiActive = active; + this.ai.activate(options); } /*---------- TWEENABLE PROPERTIES ----------*/ @@ -306,8 +317,13 @@ export default abstract class GameNode implements Positioned, Unique, Updateable /** Called if the position vector is modified or replaced */ protected positionChanged(): void { - if(this.hasPhysics){ - this.collisionShape.center = this.position.clone().add(this.colliderOffset); + if(this.collisionShape){ + if(this.colliderOffset){ + this.collisionShape.center = this.position.clone().add(this.colliderOffset); + } else { + this.collisionShape.center = this.position.clone(); + } + } }; @@ -336,15 +352,20 @@ export default abstract class GameNode implements Positioned, Unique, Updateable } // If this has a collider, draw it - if(this.hasPhysics && this.collisionShape){ + if(this.collisionShape){ let color = this.isColliding ? Color.RED : Color.GREEN; if(this.isTrigger){ - color = Color.PURPLE; + color = Color.MAGENTA; } color.a = 0.2; - Debug.drawBox(this.inRelativeCoordinates(this.collisionShape.center), this.collisionShape.halfSize.scaled(this.scene.getViewScale()), true, color); + + if(this.collisionShape instanceof AABB){ + Debug.drawBox(this.inRelativeCoordinates(this.collisionShape.center), this.collisionShape.halfSize.scaled(this.scene.getViewScale()), true, color); + } else if(this.collisionShape instanceof Circle){ + Debug.drawCircle(this.inRelativeCoordinates(this.collisionShape.center), this.collisionShape.hw*this.scene.getViewScale(), true, color); + } } } } diff --git a/src/Wolfie2D/Nodes/UIElement.ts b/src/Wolfie2D/Nodes/UIElement.ts index 35bd508..02aec9a 100644 --- a/src/Wolfie2D/Nodes/UIElement.ts +++ b/src/Wolfie2D/Nodes/UIElement.ts @@ -82,7 +82,7 @@ export default abstract class UIElement extends CanvasNode { // See of this object was just clicked if(Input.isMouseJustPressed()){ let clickPos = Input.getMousePressPosition(); - if(this.contains(clickPos.x, clickPos.y)){ + if(this.contains(clickPos.x, clickPos.y) && this.visible && !this.layer.isHidden()){ this.isClicked = true; if(this.onClick !== null){ @@ -136,15 +136,15 @@ export default abstract class UIElement extends CanvasNode { * Overridable method for calculating background color - useful for elements that want to be colored on different after certain events * @returns The background color of the UIElement */ - calculateBackgroundColor(): string { - return this.backgroundColor.toStringRGBA(); + calculateBackgroundColor(): Color { + return this.backgroundColor; } /** * Overridable method for calculating border color - useful for elements that want to be colored on different after certain events * @returns The border color of the UIElement */ - calculateBorderColor(): string { - return this.borderColor.toStringRGBA(); + calculateBorderColor(): Color { + return this.borderColor; } } \ No newline at end of file diff --git a/src/Wolfie2D/Nodes/UIElements/Button.ts b/src/Wolfie2D/Nodes/UIElements/Button.ts index 4b9db1f..8953854 100644 --- a/src/Wolfie2D/Nodes/UIElements/Button.ts +++ b/src/Wolfie2D/Nodes/UIElements/Button.ts @@ -14,14 +14,14 @@ export default class Button extends Label { } // @override - calculateBackgroundColor(): string { + calculateBackgroundColor(): Color { // Change the background color if clicked or hovered if(this.isEntered && !this.isClicked){ - return this.backgroundColor.lighten().toStringRGBA(); + return this.backgroundColor.lighten(); } else if(this.isClicked){ - return this.backgroundColor.darken().toStringRGBA(); + return this.backgroundColor.darken(); } else { - return this.backgroundColor.toStringRGBA(); + return this.backgroundColor; } } } \ No newline at end of file diff --git a/src/Wolfie2D/Nodes/UIElements/Label.ts b/src/Wolfie2D/Nodes/UIElements/Label.ts index 37e558c..b680401 100644 --- a/src/Wolfie2D/Nodes/UIElements/Label.ts +++ b/src/Wolfie2D/Nodes/UIElements/Label.ts @@ -68,6 +68,10 @@ export default class Label extends UIElement{ return ctx.measureText(this.text).width; } + setHAlign(align: string): void { + this.hAlign = align; + } + /** * Calculate the offset of the text - this is used for rendering text with different alignments * @param ctx The rendering context diff --git a/src/Wolfie2D/Registry/Registries/ShaderRegistry.ts b/src/Wolfie2D/Registry/Registries/ShaderRegistry.ts index 33d4636..ec88e11 100644 --- a/src/Wolfie2D/Registry/Registries/ShaderRegistry.ts +++ b/src/Wolfie2D/Registry/Registries/ShaderRegistry.ts @@ -1,5 +1,6 @@ import Map from "../../DataTypes/Map"; import ShaderType from "../../Rendering/WebGLRendering/ShaderType"; +import LabelShaderType from "../../Rendering/WebGLRendering/ShaderTypes/LabelShaderType"; import PointShaderType from "../../Rendering/WebGLRendering/ShaderTypes/PointShaderType"; import RectShaderType from "../../Rendering/WebGLRendering/ShaderTypes/RectShaderType"; import SpriteShaderType from "../../Rendering/WebGLRendering/ShaderTypes/SpriteShaderType"; @@ -14,6 +15,7 @@ export default class ShaderRegistry extends Map { public static POINT_SHADER = "point"; public static RECT_SHADER = "rect"; public static SPRITE_SHADER = "sprite"; + public static LABEL_SHADER = "label"; private registryItems: Array = new Array(); @@ -21,8 +23,6 @@ export default class ShaderRegistry extends Map { * Preloads all built-in shaders */ public preload(){ - console.log("Preloading"); - // Get the resourceManager and queue all built-in shaders for preloading const rm = ResourceManager.getInstance(); @@ -35,17 +35,17 @@ export default class ShaderRegistry extends Map { // Queue a load for the sprite shader this.registerAndPreloadItem(ShaderRegistry.SPRITE_SHADER, SpriteShaderType, "builtin/shaders/sprite.vshader", "builtin/shaders/sprite.fshader"); + // Queue a load for the label shader + this.registerAndPreloadItem(ShaderRegistry.LABEL_SHADER, LabelShaderType, "builtin/shaders/label.vshader", "builtin/shaders/label.fshader"); + // Queue a load for any preloaded items for(let item of this.registryItems){ const shader = new item.constr(item.key); shader.initBufferObject(); this.add(item.key, shader); - console.log("Added", item.key); - // Load if desired if(item.preload !== undefined){ - console.log("Preloading", item.key); rm.shader(item.key, item.preload.vshaderLocation, item.preload.fshaderLocation); } } diff --git a/src/Wolfie2D/Rendering/CanvasRenderer.ts b/src/Wolfie2D/Rendering/CanvasRenderer.ts index 6a72b69..d6f5433 100644 --- a/src/Wolfie2D/Rendering/CanvasRenderer.ts +++ b/src/Wolfie2D/Rendering/CanvasRenderer.ts @@ -19,6 +19,7 @@ import Slider from "../Nodes/UIElements/Slider"; import TextInput from "../Nodes/UIElements/TextInput"; import AnimatedSprite from "../Nodes/Sprites/AnimatedSprite"; import Vec2 from "../DataTypes/Vec2"; +import Color from "../Utils/Color"; /** * An implementation of the RenderingManager class using CanvasRenderingContext2D. @@ -32,6 +33,8 @@ export default class CanvasRenderer extends RenderingManager { protected origin: Vec2; protected zoom: number; + protected worldSize: Vec2; + constructor(){ super(); } @@ -49,6 +52,8 @@ export default class CanvasRenderer extends RenderingManager { canvas.width = width; canvas.height = height; + this.worldSize = new Vec2(width, height); + this.ctx = canvas.getContext("2d"); this.graphicRenderer = new GraphicRenderer(this.ctx); @@ -107,7 +112,10 @@ export default class CanvasRenderer extends RenderingManager { } // Render the uiLayers on top of everything else - uiLayers.forEach(key => uiLayers.get(key).getItems().forEach(node => this.renderNode(node))); + uiLayers.forEach(key => { + if(!uiLayers.get(key).isHidden()) + uiLayers.get(key).getItems().forEach(node => this.renderNode(node)) + }); } /** @@ -221,4 +229,10 @@ export default class CanvasRenderer extends RenderingManager { this.uiElementRenderer.renderTextInput(uiElement); } } + + clear(clearColor: Color): void { + this.ctx.clearRect(0, 0, this.worldSize.x, this.worldSize.y); + this.ctx.fillStyle = clearColor.toString(); + this.ctx.fillRect(0, 0, this.worldSize.x, this.worldSize.y); + } } \ No newline at end of file diff --git a/src/Wolfie2D/Rendering/CanvasRendering/UIElementRenderer.ts b/src/Wolfie2D/Rendering/CanvasRendering/UIElementRenderer.ts index f57cce9..69b02e1 100644 --- a/src/Wolfie2D/Rendering/CanvasRendering/UIElementRenderer.ts +++ b/src/Wolfie2D/Rendering/CanvasRendering/UIElementRenderer.ts @@ -45,11 +45,11 @@ export default class UIElementRenderer { // Stroke and fill a rounded rect and give it text this.ctx.globalAlpha = label.backgroundColor.a; - this.ctx.fillStyle = label.calculateBackgroundColor(); + this.ctx.fillStyle = label.calculateBackgroundColor().toStringRGBA(); this.ctx.fillRoundedRect(-label.size.x/2, -label.size.y/2, label.size.x, label.size.y, label.borderRadius); - this.ctx.strokeStyle = label.calculateBorderColor(); + this.ctx.strokeStyle = label.calculateBorderColor().toStringRGBA(); this.ctx.globalAlpha = label.borderColor.a; this.ctx.lineWidth = label.borderWidth; this.ctx.strokeRoundedRect(-label.size.x/2, -label.size.y/2, diff --git a/src/Wolfie2D/Rendering/RenderingManager.ts b/src/Wolfie2D/Rendering/RenderingManager.ts index 62f1605..3f4f91d 100644 --- a/src/Wolfie2D/Rendering/RenderingManager.ts +++ b/src/Wolfie2D/Rendering/RenderingManager.ts @@ -8,6 +8,7 @@ import UIElement from "../Nodes/UIElement"; import ResourceManager from "../ResourceManager/ResourceManager"; import UILayer from "../Scene/Layers/UILayer"; import Scene from "../Scene/Scene"; +import Color from "../Utils/Color"; /** * An abstract framework to put all rendering in once place in the application @@ -48,6 +49,9 @@ export default abstract class RenderingManager { */ abstract render(visibleSet: Array, tilemaps: Array, uiLayers: Map): void; + /** Clears the canvas */ + abstract clear(color: Color): void; + /** * Renders a sprite * @param sprite The sprite to render diff --git a/src/Wolfie2D/Rendering/WebGLRenderer.ts b/src/Wolfie2D/Rendering/WebGLRenderer.ts index 1e91d09..1157146 100644 --- a/src/Wolfie2D/Rendering/WebGLRenderer.ts +++ b/src/Wolfie2D/Rendering/WebGLRenderer.ts @@ -1,6 +1,7 @@ import Graph from "../DataTypes/Graphs/Graph"; import Map from "../DataTypes/Map"; import Vec2 from "../DataTypes/Vec2"; +import Debug from "../Debug/Debug"; import CanvasNode from "../Nodes/CanvasNode"; import Graphic from "../Nodes/Graphic"; import { GraphicType } from "../Nodes/Graphics/GraphicTypes"; @@ -10,10 +11,13 @@ import AnimatedSprite from "../Nodes/Sprites/AnimatedSprite"; import Sprite from "../Nodes/Sprites/Sprite"; import Tilemap from "../Nodes/Tilemap"; import UIElement from "../Nodes/UIElement"; +import Label from "../Nodes/UIElements/Label"; import ShaderRegistry from "../Registry/Registries/ShaderRegistry"; import Registry from "../Registry/Registry"; import ResourceManager from "../ResourceManager/ResourceManager"; +import ParallaxLayer from "../Scene/Layers/ParallaxLayer"; import UILayer from "../Scene/Layers/UILayer"; +import Color from "../Utils/Color"; import RenderingUtils from "../Utils/RenderingUtils"; import RenderingManager from "./RenderingManager"; import ShaderType from "./WebGLRendering/ShaderType"; @@ -25,6 +29,7 @@ export default class WebGLRenderer extends RenderingManager { protected worldSize: Vec2; protected gl: WebGLRenderingContext; + protected textCtx: CanvasRenderingContext2D; initializeCanvas(canvas: HTMLCanvasElement, width: number, height: number): WebGLRenderingContext { canvas.width = width; @@ -47,6 +52,15 @@ export default class WebGLRenderer extends RenderingManager { // Tell the resource manager we're using WebGL ResourceManager.getInstance().useWebGL(true, this.gl); + // Show the text canvas and get its context + let textCanvas = document.getElementById("text-canvas"); + textCanvas.hidden = false; + this.textCtx = textCanvas.getContext("2d"); + + // Size the text canvas to be the same as the game canvas + textCanvas.height = height; + textCanvas.width = width; + return this.gl; } @@ -54,6 +68,18 @@ export default class WebGLRenderer extends RenderingManager { for(let node of visibleSet){ this.renderNode(node); } + + uiLayers.forEach(key => { + if(!uiLayers.get(key).isHidden()) + uiLayers.get(key).getItems().forEach(node => this.renderNode(node)) + }); + } + + clear(color: Color): void { + this.gl.clearColor(color.r, color.g, color.b, color.a); + this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); + + this.textCtx.clearRect(0, 0, this.worldSize.x, this.worldSize.y); } protected renderNode(node: CanvasNode): void { @@ -74,44 +100,32 @@ export default class WebGLRenderer extends RenderingManager { } else { this.renderSprite(node); } + } else if(node instanceof UIElement){ + this.renderUIElement(node); } } protected renderSprite(sprite: Sprite): void { let shader = Registry.shaders.get(ShaderRegistry.SPRITE_SHADER); - - let options = shader.getOptions(sprite); - options.worldSize = this.worldSize; - options.origin = this.origin; - + let options = this.addOptions(shader.getOptions(sprite), sprite); shader.render(this.gl, options); } protected renderAnimatedSprite(sprite: AnimatedSprite): void { let shader = Registry.shaders.get(ShaderRegistry.SPRITE_SHADER); - - let options = shader.getOptions(sprite); - options.worldSize = this.worldSize; - options.origin = this.origin; - - Registry.shaders.get(ShaderRegistry.SPRITE_SHADER).render(this.gl, options); + let options = this.addOptions(shader.getOptions(sprite), sprite); + shader.render(this.gl, options); } protected renderGraphic(graphic: Graphic): void { if(graphic instanceof Point){ let shader = Registry.shaders.get(ShaderRegistry.POINT_SHADER); - let options = shader.getOptions(graphic); - options.worldSize = this.worldSize; - options.origin = this.origin; - + let options = this.addOptions(shader.getOptions(graphic), graphic); shader.render(this.gl, options); } else if(graphic instanceof Rect) { let shader = Registry.shaders.get(ShaderRegistry.RECT_SHADER); - let options = shader.getOptions(graphic); - options.worldSize = this.worldSize; - options.origin = this.origin; - + let options = this.addOptions(shader.getOptions(graphic), graphic); shader.render(this.gl, options); } } @@ -121,16 +135,48 @@ export default class WebGLRenderer extends RenderingManager { } protected renderUIElement(uiElement: UIElement): void { - throw new Error("Method not implemented."); + if(uiElement instanceof Label){ + let shader = Registry.shaders.get(ShaderRegistry.LABEL_SHADER); + let options = this.addOptions(shader.getOptions(uiElement), uiElement); + shader.render(this.gl, options); + + this.textCtx.setTransform(1, 0, 0, 1, (uiElement.position.x - this.origin.x)*this.zoom, (uiElement.position.y - this.origin.y)*this.zoom); + this.textCtx.rotate(-uiElement.rotation); + let globalAlpha = this.textCtx.globalAlpha; + this.textCtx.globalAlpha = uiElement.alpha; + + // Render text + this.textCtx.font = uiElement.getFontString(); + let offset = uiElement.calculateTextOffset(this.textCtx); + this.textCtx.fillStyle = uiElement.calculateTextColor(); + this.textCtx.globalAlpha = uiElement.textColor.a; + this.textCtx.fillText(uiElement.text, offset.x - uiElement.size.x/2, offset.y - uiElement.size.y/2); + + this.textCtx.globalAlpha = globalAlpha; + this.textCtx.setTransform(1, 0, 0, 1, 0, 0); + } } protected renderCustom(node: CanvasNode): void { let shader = Registry.shaders.get(node.customShaderKey); - let options = shader.getOptions(node); - options.worldSize = this.worldSize; - options.origin = this.origin; - + let options = this.addOptions(shader.getOptions(node), node); shader.render(this.gl, options); } + protected addOptions(options: Record, node: CanvasNode): Record { + // Give the shader access to the world size + options.worldSize = this.worldSize; + + // Adjust the origin position to the parallax + let layer = node.getLayer(); + let parallax = new Vec2(1, 1); + if(layer instanceof ParallaxLayer){ + parallax = (layer).parallax; + } + + options.origin = this.origin.clone().mult(parallax); + + return options; + } + } \ No newline at end of file diff --git a/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/LabelShaderType.ts b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/LabelShaderType.ts new file mode 100644 index 0000000..7582ec0 --- /dev/null +++ b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/LabelShaderType.ts @@ -0,0 +1,122 @@ +import Mat4x4 from "../../../DataTypes/Mat4x4"; +import Vec2 from "../../../DataTypes/Vec2"; +import Debug from "../../../Debug/Debug"; +import Rect from "../../../Nodes/Graphics/Rect"; +import Label from "../../../Nodes/UIElements/Label"; +import ResourceManager from "../../../ResourceManager/ResourceManager"; +import QuadShaderType from "./QuadShaderType"; + +export default class LabelShaderType extends QuadShaderType { + + constructor(programKey: string){ + super(programKey); + this.resourceManager = ResourceManager.getInstance(); + } + + initBufferObject(): void { + this.bufferObjectKey = "label"; + this.resourceManager.createBuffer(this.bufferObjectKey); + } + + render(gl: WebGLRenderingContext, options: Record): void { + const backgroundColor = options.backgroundColor.toWebGL(); + const borderColor = options.borderColor.toWebGL(); + + const program = this.resourceManager.getShaderProgram(this.programKey); + const buffer = this.resourceManager.getBuffer(this.bufferObjectKey); + + gl.useProgram(program); + + const vertexData = this.getVertices(options.size.x, options.size.y); + + const FSIZE = vertexData.BYTES_PER_ELEMENT; + + // Bind the buffer + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW); + + // Attributes + const a_Position = gl.getAttribLocation(program, "a_Position"); + gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 2 * FSIZE, 0 * FSIZE); + gl.enableVertexAttribArray(a_Position); + + // Uniforms + const u_BackgroundColor = gl.getUniformLocation(program, "u_BackgroundColor"); + gl.uniform4fv(u_BackgroundColor, backgroundColor); + + const u_BorderColor = gl.getUniformLocation(program, "u_BorderColor"); + gl.uniform4fv(u_BorderColor, borderColor); + + const u_MaxSize = gl.getUniformLocation(program, "u_MaxSize"); + gl.uniform2f(u_MaxSize, -vertexData[0], vertexData[1]); + + // Get transformation matrix + // We want a square for our rendering space, so get the maximum dimension of our quad + let maxDimension = Math.max(options.size.x, options.size.y); + + const u_BorderWidth = gl.getUniformLocation(program, "u_BorderWidth"); + gl.uniform1f(u_BorderWidth, options.borderWidth/maxDimension); + + const u_BorderRadius = gl.getUniformLocation(program, "u_BorderRadius"); + gl.uniform1f(u_BorderRadius, options.borderRadius/maxDimension); + + // The size of the rendering space will be a square with this maximum dimension + let size = new Vec2(maxDimension, maxDimension).scale(2/options.worldSize.x, 2/options.worldSize.y); + + // Center our translations around (0, 0) + const translateX = (options.position.x - options.origin.x - options.worldSize.x/2)/maxDimension; + const translateY = -(options.position.y - options.origin.y - options.worldSize.y/2)/maxDimension; + + // Create our transformation matrix + this.translation.translate(new Float32Array([translateX, translateY])); + this.scale.scale(size); + this.rotation.rotate(options.rotation); + let transformation = Mat4x4.MULT(this.translation, this.scale, this.rotation); + + // Pass the translation matrix to our shader + const u_Transform = gl.getUniformLocation(program, "u_Transform"); + gl.uniformMatrix4fv(u_Transform, false, transformation.toArray()); + + // Draw the quad + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + } + + /** + * The rendering space always has to be a square, so make sure its square w.r.t to the largest dimension + * @param w The width of the quad in pixels + * @param h The height of the quad in pixels + * @returns An array of the vertices of the quad + */ + getVertices(w: number, h: number): Float32Array { + let x, y; + + if(h > w){ + y = 0.5; + x = w/(2*h); + } else { + x = 0.5; + y = h/(2*w); + } + + return new Float32Array([ + -x, y, + -x, -y, + x, y, + x, -y + ]); + } + + getOptions(rect: Label): Record { + let options: Record = { + position: rect.position, + backgroundColor: rect.calculateBackgroundColor(), + borderColor: rect.calculateBorderColor(), + borderWidth: rect.borderWidth, + borderRadius: rect.borderRadius, + size: rect.size, + rotation: rect.rotation + } + + return options; + } +} \ No newline at end of file diff --git a/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/SpriteShaderType.ts b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/SpriteShaderType.ts index 3753745..511d39a 100644 --- a/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/SpriteShaderType.ts +++ b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/SpriteShaderType.ts @@ -22,24 +22,9 @@ export default class SpriteShaderType extends QuadShaderType { const program = this.resourceManager.getShaderProgram(this.programKey); const buffer = this.resourceManager.getBuffer(this.bufferObjectKey); const texture = this.resourceManager.getTexture(options.imageKey); - const image = this.resourceManager.getImage(options.imageKey); gl.useProgram(program); - // Enable texture0 - gl.activeTexture(gl.TEXTURE0); - - // Bind our texture to texture 0 - gl.bindTexture(gl.TEXTURE_2D, texture); - - // Set the texture parameters - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - - // Set the texture image - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); - const vertexData = this.getVertices(options.size.x, options.size.y, options.scale); const FSIZE = vertexData.BYTES_PER_ELEMENT; @@ -79,9 +64,9 @@ export default class SpriteShaderType extends QuadShaderType { const u_Transform = gl.getUniformLocation(program, "u_Transform"); gl.uniformMatrix4fv(u_Transform, false, transformation.toArray()); - // Set texture unit 0 to the sampler + // Set up our sampler with our assigned texture unit const u_Sampler = gl.getUniformLocation(program, "u_Sampler"); - gl.uniform1i(u_Sampler, 0); + gl.uniform1i(u_Sampler, texture); // Pass in texShift const u_texShift = gl.getUniformLocation(program, "u_texShift"); @@ -92,7 +77,7 @@ export default class SpriteShaderType extends QuadShaderType { gl.uniform2fv(u_texScale, options.texScale); // Draw the quad - gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4 ); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); } /** diff --git a/src/Wolfie2D/ResourceManager/ResourceManager.ts b/src/Wolfie2D/ResourceManager/ResourceManager.ts index 6532496..031c644 100644 --- a/src/Wolfie2D/ResourceManager/ResourceManager.ts +++ b/src/Wolfie2D/ResourceManager/ResourceManager.ts @@ -5,7 +5,6 @@ import StringUtils from "../Utils/StringUtils"; import AudioManager from "../Sound/AudioManager"; import Spritesheet from "../DataTypes/Spritesheet"; import WebGLProgramType from "../DataTypes/Rendering/WebGLProgramType"; -import PhysicsManager from "../Physics/PhysicsManager"; /** * The resource manager for the game engine. @@ -80,7 +79,8 @@ export default class ResourceManager { private gl_DefaultShaderPrograms: Map; private gl_ShaderPrograms: Map; - private gl_Textures: Map; + private gl_Textures: Map; + private gl_NextTextureID: number; private gl_Buffers: Map; private gl: WebGLRenderingContext; @@ -117,6 +117,7 @@ export default class ResourceManager { this.gl_ShaderPrograms = new Map(); this.gl_Textures = new Map(); + this.gl_NextTextureID = 0; this.gl_Buffers = new Map(); }; @@ -226,7 +227,7 @@ export default class ResourceManager { * @param callback The function to cal when the resources are finished loading */ loadResourcesFromQueue(callback: Function): void { - this.loadonly_typesToLoad = 3; + this.loadonly_typesToLoad = 5; this.loading = true; @@ -443,7 +444,9 @@ export default class ResourceManager { this.images.add(key, image); // If WebGL is active, create a texture - this.createWebGLTexture(key); + if(this.gl_WebGLActive){ + this.createWebGLTexture(key, image); + } // Finish image load this.finishLoadingImage(callbackIfLast); @@ -526,7 +529,7 @@ export default class ResourceManager { /* ########## WEBGL SPECIFIC FUNCTIONS ########## */ - public getTexture(key: string): WebGLTexture { + public getTexture(key: string): number { return this.gl_Textures.get(key); } @@ -538,10 +541,49 @@ export default class ResourceManager { return this.gl_Buffers.get(key); } - private createWebGLTexture(key:string): void { - if(this.gl_WebGLActive){ - const texture = this.gl.createTexture(); - this.gl_Textures.add(key, texture); + private createWebGLTexture(imageKey: string, image: HTMLImageElement): void { + // Get the texture ID + const textureID = this.getTextureID(this.gl_NextTextureID); + + // Create the texture + const texture = this.gl.createTexture(); + + // Set up the texture + // Enable texture0 + this.gl.activeTexture(textureID); + + // Bind our texture to texture 0 + this.gl.bindTexture(this.gl.TEXTURE_2D, texture); + + // Set the texture parameters + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE); + + // Set the texture image + this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, image); + + // Add the texture to our map with the same key as the image + this.gl_Textures.add(imageKey, this.gl_NextTextureID); + + // Increment the key + this.gl_NextTextureID += 1; + } + + private getTextureID(id: number): number { + // Start with 9 cases - this can be expanded if needed, but for the best performance, + // Textures should be stitched into an atlas + switch(id){ + case 0: return this.gl.TEXTURE0; + case 1: return this.gl.TEXTURE1; + case 2: return this.gl.TEXTURE2; + case 3: return this.gl.TEXTURE3; + case 4: return this.gl.TEXTURE4; + case 5: return this.gl.TEXTURE5; + case 6: return this.gl.TEXTURE6; + case 7: return this.gl.TEXTURE7; + case 8: return this.gl.TEXTURE8; + default: return this.gl.TEXTURE9; } } diff --git a/src/Wolfie2D/Scene/Scene.ts b/src/Wolfie2D/Scene/Scene.ts index 8dcbcde..0b2f506 100644 --- a/src/Wolfie2D/Scene/Scene.ts +++ b/src/Wolfie2D/Scene/Scene.ts @@ -94,7 +94,7 @@ export default class Scene implements Updateable { * @param options The options for Scene initialization */ constructor(viewport: Viewport, sceneManager: SceneManager, renderingManager: RenderingManager, options: Record){ - this.sceneOptions = SceneOptions.parse(options); + this.sceneOptions = SceneOptions.parse(options? options : {}); this.worldSize = new Vec2(500, 500); this.viewport = viewport; @@ -121,6 +121,9 @@ export default class Scene implements Updateable { this.load = ResourceManager.getInstance(); } + /** A lifecycle method that gets called immediately after a new scene is created, before anything else. */ + initScene(init: Record): void {} + /** A lifecycle method that gets called when a new scene is created. Load all files you wish to access in the scene here. */ loadScene(): void {} diff --git a/src/Wolfie2D/Scene/SceneManager.ts b/src/Wolfie2D/Scene/SceneManager.ts index 331c07c..e7d983a 100644 --- a/src/Wolfie2D/Scene/SceneManager.ts +++ b/src/Wolfie2D/Scene/SceneManager.ts @@ -1,7 +1,6 @@ import Scene from "./Scene"; import ResourceManager from "../ResourceManager/ResourceManager"; import Viewport from "../SceneGraph/Viewport"; -import Game from "../Loop/Game"; import RenderingManager from "../Rendering/RenderingManager"; /** @@ -41,11 +40,14 @@ export default class SceneManager { * Add a scene as the main scene. * Use this method if you've created a subclass of Scene, and you want to add it as the main Scene. * @param constr The constructor of the scene to add + * @param init An object to pass to the init function of the new scene */ - public addScene(constr: new (...args: any) => T, options: Record): void { + public addScene(constr: new (...args: any) => T, init?: Record, options?: Record): void { let scene = new constr(this.viewport, this, this.renderingManager, options); this.currentScene = scene; + scene.initScene(init); + // Enqueue all scene asset loads scene.loadScene(); @@ -64,16 +66,12 @@ export default class SceneManager { * Change from the current scene to this new scene. * Use this method if you've created a subclass of Scene, and you want to add it as the main Scene. * @param constr The constructor of the scene to change to + * @param init An object to pass to the init function of the new scene */ - public changeScene(constr: new (...args: any) => T, options: Record): void { - // unload current scene - this.currentScene.unloadScene(); + public changeScene(constr: new (...args: any) => T, init?: Record, options?: Record): void { + this.viewport.setCenter(this.viewport.getHalfSize().x, this.viewport.getHalfSize().y); - this.resourceManager.unloadAllResources(); - - this.viewport.setCenter(0, 0); - - this.addScene(constr, options); + this.addScene(constr, init, options); } /** @@ -88,9 +86,7 @@ export default class SceneManager { * Renders the current Scene */ public render(): void { - if(this.currentScene.isRunning()){ - this.currentScene.render(); - } + this.currentScene.render(); } /** diff --git a/src/Wolfie2D/SceneGraph/Viewport.ts b/src/Wolfie2D/SceneGraph/Viewport.ts index 320d469..6e85aee 100644 --- a/src/Wolfie2D/SceneGraph/Viewport.ts +++ b/src/Wolfie2D/SceneGraph/Viewport.ts @@ -49,14 +49,10 @@ export default class Viewport { // Set the size of the canvas this.setCanvasSize(canvasSize); - console.log(canvasSize, zoomLevel); - // Set the size of the viewport this.setSize(canvasSize); this.setZoomLevel(zoomLevel); - console.log(this.getHalfSize().toString()); - // Set the center (and make the viewport stay there) this.setCenter(this.view.halfSize.clone()); this.setFocus(this.view.halfSize.clone()); diff --git a/src/Wolfie2D/Utils/Color.ts b/src/Wolfie2D/Utils/Color.ts index 8f936b0..2bbbce9 100644 --- a/src/Wolfie2D/Utils/Color.ts +++ b/src/Wolfie2D/Utils/Color.ts @@ -69,10 +69,10 @@ export default class Color { } /** - * Purple color + * Magenta color * @returns rgb(255, 0, 255) */ - static get PURPLE(): Color { + static get MAGENTA(): Color { return new Color(255, 0, 255, 1); } @@ -127,7 +127,7 @@ export default class Color { * @returns A new lighter Color */ lighten(): Color { - return new Color(MathUtils.clamp(this.r + 40, 0, 255), MathUtils.clamp(this.g + 40, 0, 255), MathUtils.clamp(this.b + 40, 0, 255), this.a); + return new Color(MathUtils.clamp(this.r + 40, 0, 255), MathUtils.clamp(this.g + 40, 0, 255), MathUtils.clamp(this.b + 40, 0, 255), MathUtils.clamp(this.a + 10, 0, 255)); } /** @@ -135,7 +135,7 @@ export default class Color { * @returns A new darker Color */ darken(): Color { - return new Color(MathUtils.clamp(this.r - 40, 0, 255), MathUtils.clamp(this.g - 40, 0, 255), MathUtils.clamp(this.b - 40, 0, 255), this.a); + return new Color(MathUtils.clamp(this.r - 40, 0, 255), MathUtils.clamp(this.g - 40, 0, 255), MathUtils.clamp(this.b - 40, 0, 255), MathUtils.clamp(this.a + 10, 0, 255)); } /** diff --git a/src/Wolfie2D/Utils/RandUtils.ts b/src/Wolfie2D/Utils/RandUtils.ts index 42c104a..a1cac17 100644 --- a/src/Wolfie2D/Utils/RandUtils.ts +++ b/src/Wolfie2D/Utils/RandUtils.ts @@ -1,6 +1,7 @@ import MathUtils from "./MathUtils"; import Color from "./Color"; import Perlin from "./Rand/Perlin"; +import Vec2 from "../DataTypes/Vec2"; class Noise { p: Perlin = new Perlin(); @@ -21,6 +22,16 @@ export default class RandUtils { static randInt(min: number, max: number): number { return Math.floor(Math.random()*(max - min) + min); } + + /** + * Generates a random float in the specified range + * @param min The min of the range (inclusive) + * @param max The max of the range (exclusive) + * @returns A random float in the range [min, max) + */ + static randFloat(min: number, max: number): number { + return Math.random()*(max - min) + min; + } /** * Generates a random hexadecimal number in the specified range @@ -43,6 +54,10 @@ export default class RandUtils { return new Color(r, g, b); } + static randVec(minX: number, maxX: number, minY: number, maxY: number): Vec2 { + return new Vec2(this.randFloat(minX, maxX), this.randFloat(minY, maxY)); + } + /** A noise generator */ static noise: Noise = new Noise(); diff --git a/src/default_scene.ts b/src/default_scene.ts index 8166edb..9281de1 100644 --- a/src/default_scene.ts +++ b/src/default_scene.ts @@ -31,7 +31,7 @@ export default class default_scene extends Scene { // The first argument is the key of the sprite (you get to decide what it is). // The second argument is the path to the actual image. // Paths start in the "dist/" folder, so start building your path from there - this.load.image("logo", "demo_assets/wolfie2d_text.png"); + this.load.image("logo", "demo_assets/images/wolfie2d_text.png"); } // startScene() is where you should build any game objects you wish to have in your scene, diff --git a/src/Platformer.ts b/src/demos/Platformer.ts similarity index 90% rename from src/Platformer.ts rename to src/demos/Platformer.ts index 0f3aabe..9eb409a 100644 --- a/src/Platformer.ts +++ b/src/demos/Platformer.ts @@ -1,7 +1,7 @@ import PlayerController from "./PlatformerPlayerController"; -import Vec2 from "./Wolfie2D/DataTypes/Vec2"; -import AnimatedSprite from "./Wolfie2D/Nodes/Sprites/AnimatedSprite"; -import Scene from "./Wolfie2D/Scene/Scene"; +import Vec2 from "../Wolfie2D/DataTypes/Vec2"; +import AnimatedSprite from "../Wolfie2D/Nodes/Sprites/AnimatedSprite"; +import Scene from "../Wolfie2D/Scene/Scene"; export default class Platformer extends Scene { private player: AnimatedSprite; diff --git a/src/PlatformerPlayerController.ts b/src/demos/PlatformerPlayerController.ts similarity index 81% rename from src/PlatformerPlayerController.ts rename to src/demos/PlatformerPlayerController.ts index 98e4648..4062488 100644 --- a/src/PlatformerPlayerController.ts +++ b/src/demos/PlatformerPlayerController.ts @@ -1,9 +1,9 @@ -import AI from "./Wolfie2D/DataTypes/Interfaces/AI"; -import Emitter from "./Wolfie2D/Events/Emitter"; -import GameEvent from "./Wolfie2D/Events/GameEvent"; -import { GameEventType } from "./Wolfie2D/Events/GameEventType"; -import Input from "./Wolfie2D/Input/Input"; -import AnimatedSprite from "./Wolfie2D/Nodes/Sprites/AnimatedSprite"; +import AI from "../Wolfie2D/DataTypes/Interfaces/AI"; +import Emitter from "../Wolfie2D/Events/Emitter"; +import GameEvent from "../Wolfie2D/Events/GameEvent"; +import { GameEventType } from "../Wolfie2D/Events/GameEventType"; +import Input from "../Wolfie2D/Input/Input"; +import AnimatedSprite from "../Wolfie2D/Nodes/Sprites/AnimatedSprite"; export default class PlayerController implements AI { protected owner: AnimatedSprite; @@ -16,6 +16,8 @@ export default class PlayerController implements AI { this.emitter = new Emitter(); } + activate(options: Record): void {} + handleEvent(event: GameEvent): void { // Do nothing for now } diff --git a/src/hw1/GradientCircleShaderType.ts b/src/hw1/GradientCircleShaderType.ts deleted file mode 100644 index adb6db0..0000000 --- a/src/hw1/GradientCircleShaderType.ts +++ /dev/null @@ -1,68 +0,0 @@ -import Map from "../Wolfie2D/DataTypes/Map"; -import Mat4x4 from "../Wolfie2D/DataTypes/Mat4x4"; -import Vec2 from "../Wolfie2D/DataTypes/Vec2"; -import RectShaderType from "../Wolfie2D/Rendering/WebGLRendering/ShaderTypes/RectShaderType"; - -/** - * The gradient circle is technically rendered on a quad, and is similar to a rect, so we'll extend the RectShaderType - */ -export default class GradientCircleShaderType extends RectShaderType { - - initBufferObject(): void { - this.bufferObjectKey = "gradient_circle"; - this.resourceManager.createBuffer(this.bufferObjectKey); - } - - render(gl: WebGLRenderingContext, options: Record): void { - // Get our program and buffer object - const program = this.resourceManager.getShaderProgram(this.programKey); - const buffer = this.resourceManager.getBuffer(this.bufferObjectKey); - - // Let WebGL know we're using our shader program - gl.useProgram(program); - - // Get our vertex data - const vertexData = this.getVertices(options.size.x, options.size.y); - const FSIZE = vertexData.BYTES_PER_ELEMENT; - - // Bind the buffer - gl.bindBuffer(gl.ARRAY_BUFFER, buffer); - gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW); - - /* ##### ATTRIBUTES ##### */ - // No texture, the only thing we care about is vertex position - const a_Position = gl.getAttribLocation(program, "a_Position"); - gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 2 * FSIZE, 0 * FSIZE); - gl.enableVertexAttribArray(a_Position); - - /* ##### UNIFORMS ##### */ - // Send the color to the gradient circle - const color = options.color.toWebGL(); - const u_Color = gl.getUniformLocation(program, "u_Color"); - gl.uniform4fv(u_Color, color); - - // Get transformation matrix - // We have a square for our rendering space, so get the maximum dimension of our quad - let maxDimension = Math.max(options.size.x, options.size.y); - - // The size of the rendering space will be a square with this maximum dimension - let size = new Vec2(maxDimension, maxDimension).scale(2/options.worldSize.x, 2/options.worldSize.y); - - // Center our translations around (0, 0) - const translateX = (options.position.x - options.origin.x - options.worldSize.x/2)/maxDimension; - const translateY = -(options.position.y - options.origin.y - options.worldSize.y/2)/maxDimension; - - // Create our transformation matrix - this.translation.translate(new Float32Array([translateX, translateY])); - this.scale.scale(size); - this.rotation.rotate(options.rotation); - let transformation = Mat4x4.MULT(this.translation, this.scale, this.rotation); - - // Pass the translation matrix to our shader - const u_Transform = gl.getUniformLocation(program, "u_Transform"); - gl.uniformMatrix4fv(u_Transform, false, transformation.toArray()); - - // Draw the quad - gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); - } -} \ No newline at end of file diff --git a/src/hw1/HW1_Enums.ts b/src/hw1/HW1_Enums.ts deleted file mode 100644 index d380546..0000000 --- a/src/hw1/HW1_Enums.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum Homework1Event { - PLAYER_DAMAGE = "PLAYER_DAMAGE", - SPAWN_FLEET = "SPAWN_FLEET" -} - -export enum Homework1Shaders { - GRADIENT_CIRCLE = "GRADIENT_CIRCLE" -} \ No newline at end of file diff --git a/src/hw1/HW1_Scene.ts b/src/hw1/HW1_Scene.ts deleted file mode 100644 index abba8e3..0000000 --- a/src/hw1/HW1_Scene.ts +++ /dev/null @@ -1,107 +0,0 @@ -import AABB from "../Wolfie2D/DataTypes/Shapes/AABB"; -import Vec2 from "../Wolfie2D/DataTypes/Vec2"; -import Graphic from "../Wolfie2D/Nodes/Graphic"; -import { GraphicType } from "../Wolfie2D/Nodes/Graphics/GraphicTypes"; -import AnimatedSprite from "../Wolfie2D/Nodes/Sprites/AnimatedSprite"; -import Scene from "../Wolfie2D/Scene/Scene"; -import { Homework1Event, Homework1Shaders } from "./HW1_Enums"; -import SpaceshipPlayerController from "./SpaceshipPlayerController"; - -/** - * In Wolfie2D, custom scenes extend the original scene class. - * This gives us access to lifecycle methods to control our game. - */ -export default class Homework1_Scene extends Scene { - // Here we define member variables of our game, and object pools for adding in game objects - private player: AnimatedSprite; - - // Create an object pool for our fleet - private MAX_FLEET_SIZE = 20; - private fleet: Array = new Array(this.MAX_FLEET_SIZE); - - // Create an object pool for our fleet - private MAX_NUM_ASTEROIDS = 6; - private asteroids: Array = new Array(this.MAX_NUM_ASTEROIDS); - - // Create an object pool for our fleet - private MAX_NUM_MINERALS = 20; - private minerals: Array = new Array(this.MAX_NUM_MINERALS); - - /* - * loadScene() overrides the parent class method. It allows us to load in custom assets for - * use in our scene. - */ - loadScene(){ - /* ##### DO NOT MODIFY ##### */ - // Load in the player spaceship spritesheet - this.load.spritesheet("player", "hw1_assets/spritesheets/player_spaceship.json"); - - /* ##### YOUR CODE GOES BELOW THIS LINE ##### */ - } - - /* - * startScene() allows us to add in the assets we loaded in loadScene() as game objects. - * Everything here happens strictly before update - */ - startScene(){ - /* ##### DO NOT MODIFY ##### */ - // Create a layer to serve as our main game - Feel free to use this for your own assets - // It is given a depth of 5 to be above our background - this.addLayer("primary", 5); - - // Add in the player as an animated sprite - // We give it the key specified in our load function and the name of the layer - this.player = this.add.animatedSprite("player", "primary"); - - // Set the player's position to the middle of the screen, and scale it down - this.player.position.set(this.viewport.getCenter().x, this.viewport.getCenter().y); - this.player.scale.set(0.5, 0.5); - - // Play the idle animation by default - this.player.animation.play("idle"); - - // Add physics to the player - let playerCollider = new AABB(Vec2.ZERO, new Vec2(64, 64)); - - // We'll specify a smaller collider centered on the player. - // Also, we don't need collision handling, so disable it. - this.player.addPhysics(playerCollider, Vec2.ZERO, false); - - // Add a a playerController to the player - this.player.addAI(SpaceshipPlayerController, {owner: this.player, spawnFleetEventKey: "spawnFleet"}); - - /* ##### YOUR CODE GOES BELOW THIS LINE ##### */ - // Initialize the fleet object pool - - // Initialize the mineral object pool - for(let i = 0; i < this.minerals.length; i++){ - this.minerals[i] = this.add.graphic(GraphicType.RECT, "primary", {position: new Vec2(0, 0), size: new Vec2(32, 32)}); - this.minerals[i].visible = false; - } - - // Initialize the asteroid object pool - let gc = this.add.graphic(GraphicType.RECT, "primary", {position: new Vec2(400, 400), size: new Vec2(100, 100)}); - gc.useCustomShader(Homework1Shaders.GRADIENT_CIRCLE); - - // Subscribe to events - this.receiver.subscribe(Homework1Event.PLAYER_DAMAGE); - this.receiver.subscribe(Homework1Event.SPAWN_FLEET); - } - - /* - * updateScene() is where the real work is done. This is where any custom behavior goes. - */ - updateScene(){ - // Handle events we care about - while(this.receiver.hasNextEvent()){ - let event = this.receiver.getNextEvent(); - } - - // Check for collisions - for(let i = 0; i < this.minerals.length; i++){ - if(this.player.collisionShape.overlaps(this.minerals[i].boundary)){ - console.log(true); - } - } - } -} \ No newline at end of file diff --git a/src/hw1/SpaceshipPlayerController.ts b/src/hw1/SpaceshipPlayerController.ts deleted file mode 100644 index e4a3ec0..0000000 --- a/src/hw1/SpaceshipPlayerController.ts +++ /dev/null @@ -1,77 +0,0 @@ -import AI from "../Wolfie2D/DataTypes/Interfaces/AI"; -import Vec2 from "../Wolfie2D/DataTypes/Vec2"; -import Debug from "../Wolfie2D/Debug/Debug"; -import Emitter from "../Wolfie2D/Events/Emitter"; -import GameEvent from "../Wolfie2D/Events/GameEvent"; -import Input from "../Wolfie2D/Input/Input"; -import AnimatedSprite from "../Wolfie2D/Nodes/Sprites/AnimatedSprite"; -import MathUtils from "../Wolfie2D/Utils/MathUtils"; -import { Homework1Event } from "./HW1_Enums"; - -export default class SpaceshipPlayerController implements AI { - // We want to be able to control our owner, so keep track of them - private owner: AnimatedSprite; - - // The direction the spaceship is moving - private direction: Vec2; - private MIN_SPEED: number = 0; - private MAX_SPEED: number = 300; - private speed: number; - private ACCELERATION: number = 4; - private rotationSpeed: number; - - // An emitter to hook into the event queue - private emitter: Emitter; - - initializeAI(owner: AnimatedSprite, options: Record): void { - this.owner = owner; - - // Start facing up - this.direction = new Vec2(0, 1); - this.speed = 0; - this.rotationSpeed = 2; - - this.emitter = new Emitter(); - } - - handleEvent(event: GameEvent): void { - // We need to handle animations when we get hurt - if(event.type === Homework1Event.PLAYER_DAMAGE){ - this.owner.animation.play("shield"); - } - } - - update(deltaT: number): void { - // We need to handle player input - let forwardAxis = (Input.isPressed('forward') ? 1 : 0) + (Input.isPressed('backward') ? -1 : 0); - let turnDirection = (Input.isPressed('turn_ccw') ? -1 : 0) + (Input.isPressed('turn_cw') ? 1 : 0); - - // Space controls - speed stays the same if nothing happens - // Forward to speed up, backward to slow down - this.speed += this.ACCELERATION * forwardAxis; - this.speed = MathUtils.clamp(this.speed, this.MIN_SPEED, this.MAX_SPEED); - - // Rotate the player - this.direction.rotateCCW(turnDirection * this.rotationSpeed * deltaT); - - // Update the visual direction of the player - this.owner.rotation = -(Math.atan2(this.direction.y, this.direction.x) - Math.PI/2); - - // Move the player with physics - this.owner.move(this.direction.scaled(-this.speed * deltaT)); - - // If the player clicked, we need to spawn in a fleet member - if(Input.isMouseJustPressed()){ - this.emitter.fireEvent(Homework1Event.SPAWN_FLEET, {position: Input.getGlobalMousePosition()}); - } - - // Animations - if(!this.owner.animation.isPlaying("shield")){ - if(this.speed > 0){ - this.owner.animation.playIfNotAlready("boost"); - } else { - this.owner.animation.playIfNotAlready("idle"); - } - } - } -} \ No newline at end of file diff --git a/src/index.html b/src/index.html index 4276b15..1d0490d 100644 --- a/src/index.html +++ b/src/index.html @@ -22,6 +22,15 @@ left: 0px; } + #text-canvas { + width: 100%; + height: 100%; + position: absolute; + top: 0px; + left: 0px; + pointer-events: none; + } + #debug-canvas { width: 100%; height: 100%; @@ -38,6 +47,8 @@
+ +
diff --git a/src/main.ts b/src/main.ts index 6acbaa2..f28d2ef 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,36 +1,24 @@ import Game from "./Wolfie2D/Loop/Game"; -import Homework1_Scene from "./hw1/HW1_Scene"; -import Registry from "./Wolfie2D/Registry/Registry"; -import { Homework1Shaders } from "./hw1/HW1_Enums"; -import GradientCircleShaderType from "./hw1/GradientCircleShaderType"; +import default_scene from "./default_scene"; // The main function is your entrypoint into Wolfie2D. Specify your first scene and any options here. (function main(){ - // Set up options - let options = { - canvasSize: {x: 1200, y: 800}, - clearColor: {r: 0.1, g: 0.1, b: 0.1}, - inputs: [ - { name: "forward", keys: ["w"] }, - { name: "backward", keys: ["s"] }, - { name: "turn_ccw", keys: ["a"] }, - { name: "turn_cw", keys: ["d"] }, - ], - useWebGL: true, - showDebug: false - } + // Run any tests + runTests(); - // We have a custom shader, so lets add it to the registry and preload it - Registry.shaders.registerAndPreloadItem( - Homework1Shaders.GRADIENT_CIRCLE, // The key of the shader program - GradientCircleShaderType, // The constructor of the shader program - "hw1_assets/shaders/gradient_circle.vshader", // The path to the vertex shader - "hw1_assets/shaders/gradient_circle.fshader"); // the path to the fragment shader + // Set up options for our game + let options = { + canvasSize: {x: 1200, y: 800}, // The size of the game + clearColor: {r: 0.1, g: 0.1, b: 0.1}, // The color the game clears to + useWebGL: true, // Tell the game we want to use webgl + showDebug: false // Whether to show debug messages. You can change this to true if you want + } // Create a game with the options specified const game = new Game(options); // Start our game - game.start(); - game.getSceneManager().addScene(Homework1_Scene, {}); -})(); \ No newline at end of file + game.start(default_scene, {}); +})(); + +function runTests(){}; \ No newline at end of file